/* * File: direct.cc * Summary: Functions used when picking squares. * Written by: Linley Henzell */ #include "AppHdr.h" #include "directn.h" #include "format.h" #include #include #include #include #include #include #include "externs.h" #include "options.h" #include "cio.h" #include "cloud.h" #include "colour.h" #include "command.h" #include "coord.h" #include "coordit.h" #include "dbg-util.h" #include "debug.h" #include "describe.h" #include "dungeon.h" #include "map_knowledge.h" #include "fprop.h" #include "invent.h" #include "itemname.h" #include "items.h" #include "l_defs.h" #include "los.h" #include "macro.h" #include "mapmark.h" #include "message.h" #include "menu.h" #include "misc.h" #include "mon-stuff.h" #include "mon-info.h" #include "mon-util.h" #include "output.h" #include "player.h" #include "shopping.h" #include "show.h" #include "showsymb.h" #include "state.h" #include "stuff.h" #include "env.h" #include "areas.h" #include "stash.h" #ifdef USE_TILE #include "tiles.h" #include "tilereg.h" #endif #include "terrain.h" #include "traps.h" #include "travel.h" #include "tutorial.h" #include "view.h" #include "viewchar.h" #include "viewgeom.h" #include "wiz-mon.h" #define SHORT_DESC_KEY "short_desc_key" typedef std::map desc_map; static desc_map base_desc_to_short; enum LOSSelect { LOS_ANY = 0x00, // Check only visible squares LOS_VISIBLE = 0x01, // Check only hidden squares LOS_HIDDEN = 0x02, LOS_VISMASK = 0x03, // Flip from visible to hidden when going forward, // or hidden to visible when going backwards. LOS_FLIPVH = 0x20, // Flip from hidden to visible when going forward, // or visible to hidden when going backwards. LOS_FLIPHV = 0x40, LOS_NONE = 0xFFFF }; static void _describe_feature(const coord_def& where, bool oos); static void _describe_cell(const coord_def& where, bool in_range = true); static bool _find_object( const coord_def& where, int mode, bool need_path, int range ); static bool _find_monster( const coord_def& where, int mode, bool need_path, int range ); static bool _find_feature( const coord_def& where, int mode, bool need_path, int range ); #ifndef USE_TILE static bool _find_mlist( const coord_def& where, int mode, bool need_path, int range ); #endif static char _find_square_wrapper(coord_def &mfp, char direction, bool (*find_targ)(const coord_def&, int, bool, int), bool need_path, int mode, int range, bool wrap, int los = LOS_ANY); static char _find_square(coord_def &mfp, int direction, bool (*find_targ)(const coord_def&, int, bool, int), bool need_path, int mode, int range, bool wrap, int los = LOS_ANY); static int _targetting_cmd_to_compass( command_type command ); static void _describe_oos_square(const coord_def& where); static void _extend_move_to_edge(dist &moves); static std::string _get_monster_desc(const monsters *mon); dist::dist() : isValid(false), isTarget(false), isMe(false), isEndpoint(false), isCancel(true), choseRay(false), target(), delta(), ray(), prev_target(MHITNOT) { } void direction_choose_compass( dist& moves, targetting_behaviour *beh) { moves.isValid = true; moves.isTarget = false; moves.isMe = false; moves.isCancel = false; moves.delta.reset(); mouse_control mc(MOUSE_MODE_TARGET_DIR); beh->compass = true; do { const command_type key_command = beh->get_command(); #if defined(USE_UNIX_SIGNALS) && defined(SIGHUP_SAVE) && defined(USE_CURSES) // If we've received a HUP signal then the user can't choose a // target. if (crawl_state.seen_hups) { moves.isValid = false; moves.isCancel = true; mpr("Targetting interrupted by HUP signal.", MSGCH_ERROR); return; } #endif #ifdef USE_TILE if (key_command == CMD_TARGET_MOUSE_MOVE) { continue; } else if (key_command == CMD_TARGET_MOUSE_SELECT) { const coord_def &gc = tiles.get_cursor(); if (gc == Region::NO_CURSOR) continue; if (!map_bounds(gc)) continue; coord_def delta = gc - you.pos(); if (delta.x < -1 || delta.x > 1 || delta.y < -1 || delta.y > 1) { tiles.place_cursor(CURSOR_MOUSE, gc); delta = tiles.get_cursor() - you.pos(); ASSERT(delta.x >= -1 && delta.x <= 1); ASSERT(delta.y >= -1 && delta.y <= 1); } moves.delta = delta; moves.isMe = delta.origin(); break; } #endif if (key_command == CMD_TARGET_SELECT) { moves.delta.reset(); moves.isMe = true; break; } const int i = _targetting_cmd_to_compass(key_command); if (i != -1) { moves.delta = Compass[i]; } else if (key_command == CMD_TARGET_CANCEL) { moves.isCancel = true; moves.isValid = false; } } while (!moves.isCancel && moves.delta.origin()); #ifdef USE_TILE tiles.place_cursor(CURSOR_MOUSE, Region::NO_CURSOR); #endif } static int _targetting_cmd_to_compass( command_type command ) { switch ( command ) { case CMD_TARGET_UP: case CMD_TARGET_DIR_UP: return 0; case CMD_TARGET_UP_RIGHT: case CMD_TARGET_DIR_UP_RIGHT: return 1; case CMD_TARGET_RIGHT: case CMD_TARGET_DIR_RIGHT: return 2; case CMD_TARGET_DOWN_RIGHT: case CMD_TARGET_DIR_DOWN_RIGHT: return 3; case CMD_TARGET_DOWN: case CMD_TARGET_DIR_DOWN: return 4; case CMD_TARGET_DOWN_LEFT: case CMD_TARGET_DIR_DOWN_LEFT: return 5; case CMD_TARGET_LEFT: case CMD_TARGET_DIR_LEFT: return 6; case CMD_TARGET_UP_LEFT: case CMD_TARGET_DIR_UP_LEFT: return 7; default: return -1; } } static int _targetting_cmd_to_feature( command_type command ) { switch ( command ) { case CMD_TARGET_FIND_TRAP: return '^'; case CMD_TARGET_FIND_PORTAL: return '\\'; case CMD_TARGET_FIND_ALTAR: return '_'; case CMD_TARGET_FIND_UPSTAIR: return '<'; case CMD_TARGET_FIND_DOWNSTAIR: return '>'; default: return 0; } } static command_type shift_direction(command_type cmd) { switch (cmd) { case CMD_TARGET_DOWN_LEFT: return CMD_TARGET_DIR_DOWN_LEFT; case CMD_TARGET_LEFT: return CMD_TARGET_DIR_LEFT; case CMD_TARGET_DOWN: return CMD_TARGET_DIR_DOWN; case CMD_TARGET_UP: return CMD_TARGET_DIR_UP; case CMD_TARGET_RIGHT: return CMD_TARGET_DIR_RIGHT; case CMD_TARGET_DOWN_RIGHT: return CMD_TARGET_DIR_DOWN_RIGHT; case CMD_TARGET_UP_RIGHT: return CMD_TARGET_DIR_UP_RIGHT; case CMD_TARGET_UP_LEFT: return CMD_TARGET_DIR_UP_LEFT; default: return (cmd); } } // Print the proper prompt while in targetting mode. // mode indicates the targetting mode we are in, and cell is where we are // currently looking at (so that we can describe it, if necessary.) static void _target_mode_prompt(const char* prompt_prefix, targetting_type mode, const coord_def& cell) { if (prompt_prefix == NULL) prompt_prefix = "Aim"; // default if none given // Find out what we're looking at. const monsters* mon_in_cell = monster_at(cell); if (mon_in_cell && !you.can_see(mon_in_cell)) mon_in_cell = NULL; // Is it our target? const bool looking_at_target = mon_in_cell && (get_current_target() == mon_in_cell); // Work out what keys we can use to hit this target (if any.) std::string hint_string; if (looking_at_target) hint_string = ", p/t - " + mon_in_cell->name(DESC_PLAIN); else if (mon_in_cell) hint_string = ", t - " + mon_in_cell->name(DESC_PLAIN); // All preparatory work done, build the prompt string. std::string prompt = prompt_prefix; prompt += " (? - help"; switch (mode) { case DIR_NONE: if (Options.target_unshifted_dirs) prompt += ", Shift-Dir - straight line"; prompt += hint_string; break; case DIR_TARGET: prompt += ", Dir - move target cursor"; prompt += hint_string; break; default: break; } prompt += ")"; // Display the prompt. mprf(MSGCH_PROMPT, "%s", prompt.c_str()); } #ifndef USE_TILE static void _draw_ray_glyph(const coord_def &pos, int colour, int glych, int mcol, bool in_range) { if (const monsters *mons = monster_at(pos)) { if (mons->alive() && mons->visible_to(&you)) { glych = get_screen_glyph(pos); colour = mcol; } } const coord_def vp = grid2view(pos); cgotoxy(vp.x, vp.y, GOTO_DNGN); textcolor( real_colour(colour) ); putch(glych); } #endif // Unseen monsters in shallow water show a "strange disturbance". // (Unless flying!) static bool _mon_submerged_in_water(const monsters *mon) { if (!mon) return (false); return (grd(mon->pos()) == DNGN_SHALLOW_WATER && you.see_cell(mon->pos()) && !mon->visible_to(&you) && !mons_flies(mon)); } static bool _mon_exposed_in_cloud(const monsters *mon) { if (!mon) return (false); return (!mon->visible_to(&you) && is_opaque_cloud(env.cgrid(mon->pos())) && !mons_is_insubstantial(mon->type)); } static bool _mon_exposed(const monsters* mon) { return (_mon_submerged_in_water(mon) || _mon_exposed_in_cloud(mon)); } static bool _is_target_in_range(const coord_def& where, int range) { // Range doesn't matter. if (range == -1) return (true); return (grid_distance(you.pos(), where) <= range); } // We handle targetting for repeating commands and re-doing the // previous command differently (i.e., not just letting the keys // stuffed into the macro buffer replay as-is) because if the player // targeted a monster using the movement keys and the monster then // moved between repetitions, then simply replaying the keys in the // buffer will target an empty square. static void _direction_again(dist& moves, targetting_type restricts, targ_mode_type mode, int range, bool just_looking, const char *prompt, targetting_behaviour *beh) { moves.isValid = false; moves.isTarget = false; moves.isMe = false; moves.isCancel = false; moves.isEndpoint = false; moves.choseRay = false; if (you.prev_targ == MHITNOT && you.prev_grd_targ.origin()) { moves.isCancel = true; crawl_state.cancel_cmd_repeat(); return; } #ifdef DEBUG int targ_types = 0; if (you.prev_targ != MHITNOT && you.prev_targ != MHITYOU) targ_types++; if (you.prev_targ == MHITYOU) targ_types++; if (you.prev_grd_targ != coord_def(0, 0)) targ_types++; ASSERT(targ_types == 1); #endif // Discard keys until we get to a set-target command command_type key_command = CMD_NO_CMD; while (crawl_state.is_replaying_keys()) { key_command = beh->get_command(); if (key_command == CMD_TARGET_PREV_TARGET || key_command == CMD_TARGET_SELECT_ENDPOINT || key_command == CMD_TARGET_SELECT || key_command == CMD_TARGET_SELECT_FORCE_ENDPOINT || key_command == CMD_TARGET_SELECT_FORCE || key_command == CMD_TARGET_MAYBE_PREV_TARGET || key_command == CMD_TARGET_MOUSE_SELECT) { break; } } if (!crawl_state.is_replaying_keys()) { moves.isCancel = true; mpr("Ran out of keys."); return; } if (key_command == CMD_TARGET_SELECT_ENDPOINT || key_command == CMD_TARGET_SELECT_FORCE_ENDPOINT) { moves.isEndpoint = true; } if (you.prev_grd_targ != coord_def(0, 0)) { if (!you.see_cell(you.prev_grd_targ)) { moves.isCancel = true; crawl_state.cancel_cmd_all("You can no longer see the dungeon " "square you previously targeted."); return; } else if (you.prev_grd_targ == you.pos()) { moves.isCancel = true; crawl_state.cancel_cmd_all("You are now standing on your " "previously targeted dungeon " "square."); return; } else if (!_is_target_in_range(you.prev_grd_targ, range)) { moves.isCancel = true; crawl_state.cancel_cmd_all("Your previous target is now out of " "range."); return; } moves.target = you.prev_grd_targ; moves.choseRay = find_ray(you.pos(), moves.target, moves.ray); } else if (you.prev_targ == MHITYOU) { moves.isMe = true; moves.target = you.pos(); // Discard 'Y' player gave to yesno() // Changed this, was that necessary? -cao //if (mode == TARG_ENEMY) if (mode == TARG_ENEMY || mode == TARG_HOSTILE) getchm(); } else { const monsters *montarget = &menv[you.prev_targ]; if (!you.can_see(montarget)) { moves.isCancel = true; crawl_state.cancel_cmd_all("Your target is gone."); return; } else if (!_is_target_in_range(montarget->pos(), range)) { moves.isCancel = true; crawl_state.cancel_cmd_all("Your previous target is now out of " "range."); return; } moves.target = montarget->pos(); moves.choseRay = find_ray(you.pos(), moves.target, moves.ray); } moves.isValid = true; moves.isTarget = true; } class view_desc_proc { public: view_desc_proc() { // This thing seems to be starting off 1 line above where it // should be. -cao nextline(); } int width() { return crawl_view.msgsz.x; } int height() { return crawl_view.msgsz.y; } void print(const std::string &str) { cprintf("%s", str.c_str()); } void nextline() { cgotoxy(1, wherey() + 1); } }; static void _describe_monster(const monsters *mon); // Lists all the monsters and items currently in view by the player. // TODO: Allow sorting of items lists. void full_describe_view() { std::vector list_mons; std::vector list_items; std::vector list_features; // Grab all items known (or thought) to be in the stashes in view. for (radius_iterator ri(you.pos(), LOS_RADIUS); ri; ++ri) { if (feat_stair_direction(grd(*ri)) != CMD_NO_CMD || feat_is_altar(grd(*ri))) { list_features.push_back(*ri); } const monsters *mon = monster_at(*ri); const bool unknown_mimic = (mon && mons_is_unknown_mimic(mon)); if (unknown_mimic) // It'll be on top. list_items.push_back(get_mimic_item(mon)); const int oid = you.visible_igrd(*ri); if (oid == NON_ITEM) continue; if (StashTracker::is_level_untrackable()) { // On levels with no stashtracker, you can still see the top // item. if (!unknown_mimic) list_items.push_back(mitm[oid]); } else { const std::vector items = item_list_in_stash(*ri); #ifdef DEBUG_DIAGNOSTICS if (items.empty()) { mprf(MSGCH_ERROR, "No items found in stash, but top item is %s", mitm[oid].name(DESC_PLAIN).c_str()); more(); } #endif list_items.insert(list_items.end(), items.begin(), items.end()); } } // Get monsters via the monster_info, sorted by difficulty. get_monster_info(list_mons); std::sort(list_mons.begin(), list_mons.end(), monster_info::less_than_wrapper); if (list_mons.empty() && list_items.empty() && list_features.empty()) { mprf("No monsters, items or features are visible."); return; } InvMenu desc_menu(MF_SINGLESELECT | MF_ANYPRINTABLE | MF_ALLOW_FORMATTING | MF_SELECT_BY_PAGE); std::string title = ""; std::string action = ""; if (!list_mons.empty()) { title = "Monsters"; action = "view"; // toggle views monster description } bool nonmons = false; if (!list_items.empty()) { if (!title.empty()) title += "/"; title += "Items"; nonmons = true; } if (!list_features.empty()) { if (!title.empty()) title += "/"; title += "Features"; nonmons = true; } if (nonmons) { if (!action.empty()) action += "/"; action += "travel"; // toggle travels to items/features } title = "Visible " + title; std::string title1 = title + " (select to " + action + ", '!' to examine):"; title += " (select for more detail, '!' to " + action + "):"; desc_menu.set_title( new MenuEntry(title, MEL_TITLE), false); desc_menu.set_title( new MenuEntry(title1, MEL_TITLE) ); desc_menu.set_tag("pickup"); desc_menu.set_type(MT_PICKUP); // necessary for sorting of the item submenu desc_menu.action_cycle = Menu::CYCLE_TOGGLE; // Don't make a menu so tall that we recycle hotkeys on the same page. if (list_mons.size() + list_items.size() + list_features.size() > 52 && (desc_menu.maxpagesize() > 52 || desc_menu.maxpagesize() == 0)) { desc_menu.set_maxpagesize(52); } // Start with hotkey 'a' and count from there. menu_letter hotkey; // Build menu entries for monsters. if (!list_mons.empty()) { desc_menu.add_entry( new MenuEntry("Monsters", MEL_SUBTITLE) ); std::vector::const_iterator mi; for (mi = list_mons.begin(); mi != list_mons.end(); ++mi) { // List monsters in the form // (A) An angel (neutral), wielding a glowing long sword std::string prefix = ""; #ifndef USE_TILE const std::string col_string = colour_to_str(mi->m_glyph_colour); prefix = "(<" + col_string + ">" + stringize_glyph(mi->m_glyph) + ") "; #endif std::string str = get_monster_equipment_desc(mi->m_mon, true, DESC_CAP_A, true); if (you.beheld_by(mi->m_mon)) str += ", keeping you mesmerised"; if (mi->m_damage_level != MDAM_OKAY) str += ", " + mi->m_damage_desc; #ifndef USE_TILE // Wraparound if the description is longer than allowed. linebreak_string2(str, get_number_of_cols() - 9); #endif std::vector fss; formatted_string::parse_string_to_multiple(str, fss); MenuEntry *me = NULL; for (unsigned int j = 0; j < fss.size(); ++j) { if (j == 0) me = new MonsterMenuEntry(prefix+str, mi->m_mon, hotkey++); #ifndef USE_TILE else { str = " " + fss[j].tostring(); me = new MenuEntry(str, MEL_ITEM, 1); } #endif desc_menu.add_entry(me); } } } // Build menu entries for items. if (!list_items.empty()) { std::vector all_items; for (unsigned int i = 0; i < list_items.size(); ++i) all_items.push_back( new InvEntry(list_items[i]) ); const menu_sort_condition *cond = desc_menu.find_menu_sort_condition(); desc_menu.sort_menu(all_items, cond); desc_menu.add_entry( new MenuEntry( "Items", MEL_SUBTITLE ) ); for (unsigned int i = 0; i < all_items.size(); ++i, hotkey++) { InvEntry *me = all_items[i]; #ifndef USE_TILE // Show glyphs only for ASCII. me->set_show_glyph(true); #endif me->tag = "pickup"; me->hotkeys[0] = hotkey; me->quantity = 2; // Hack to make items selectable. desc_menu.add_entry(me); } } if (!list_features.empty()) { desc_menu.add_entry( new MenuEntry("Features", MEL_SUBTITLE) ); for (unsigned int i = 0; i < list_features.size(); ++i, hotkey++) { const coord_def c = list_features[i]; std::string desc = ""; #ifndef USE_TILE const coord_def e = c - you.pos() + coord_def(8,8); glyph g = get_show_glyph(env.show(e)); const std::string colour_str = colour_to_str(g.col); desc = "(<" + colour_str + ">"; desc += stringize_glyph(g.ch); if (g.ch == '<') desc += '<'; desc += ") "; #endif desc += feature_description(c); if (is_unknown_stair(c)) desc += " (not visited)"; FeatureMenuEntry *me = new FeatureMenuEntry(desc, c, hotkey); me->tag = "description"; // Hack to make features selectable. me->quantity = c.x*100 + c.y + 3; desc_menu.add_entry(me); } } // Select an item to read its full description, or a monster to read its // e'x'amine description. Toggle with '!' to travel to an item's position // or read a monster's database entry. // (Maybe that should be reversed in the case of monsters.) // For ASCII, the 'x' information may include short database descriptions. // Menu loop std::vector sel; while (true) { sel = desc_menu.show(); redraw_screen(); if (sel.empty()) break; // HACK: quantity == 1: monsters, quantity == 2: items const int quant = sel[0]->quantity; if (quant == 1) { // Get selected monster. monsters* m = (monsters*)(sel[0]->data); #ifdef USE_TILE // Highlight selected monster on the screen. const coord_def gc(m->pos()); tiles.place_cursor(CURSOR_TUTORIAL, gc); const std::string &desc = get_terse_square_desc(gc); tiles.clear_text_tags(TAG_TUTORIAL); tiles.add_text_tag(TAG_TUTORIAL, desc, gc); #endif if (desc_menu.menu_action == InvMenu::ACT_EXAMINE) { describe_info inf; get_square_desc(m->pos(), inf, true); #ifndef USE_TILE // Hmpf. This was supposed to work for both ASCII *and* Tiles! view_desc_proc proc; process_description(proc, inf); #else mesclr(); _describe_monster(m); #endif if (getch() == 0) getch(); } else // ACT_EXECUTE, here used to view database entry { describe_monsters(*m); redraw_screen(); mesclr(true); } } else if (quant == 2) { // Get selected item. item_def* i = (item_def*)(sel[0]->data); if (desc_menu.menu_action == InvMenu::ACT_EXAMINE) describe_item( *i ); else // ACT_EXECUTE -> travel to item { const coord_def c = i->pos; start_travel( c ); break; } } else { const int num = quant - 3; const int y = num % 100; const int x = (num - y)/100; coord_def c(x,y); if (desc_menu.menu_action == InvMenu::ACT_EXAMINE) describe_feature_wide(c); else // ACT_EXECUTE -> travel to feature { start_travel( c ); break; } } } #ifndef USE_TILE if (!list_items.empty()) { // Unset show_glyph for other menus. InvEntry *me = new InvEntry(list_items[0]); me->set_show_glyph(false); delete me; } #else // Clear cursor placement. tiles.place_cursor(CURSOR_TUTORIAL, Region::NO_CURSOR); tiles.clear_text_tags(TAG_TUTORIAL); #endif } //--------------------------------------------------------------- // // direction // // use restrict == DIR_DIR to allow only a compass direction; // == DIR_TARGET to allow only choosing a square; // == DIR_NONE to allow either. // // outputs: dist structure: // // isValid a valid target or direction was chosen // isCancel player hit 'escape' // isTarget targetting was used // choseRay player wants a specific ray // ray ...this one // isEndpoint player wants the ray to stop on the dime // tx,ty target x,y // dx,dy direction delta for DIR_DIR // //-------------------------------------------------------------- #ifndef USE_TILE // XXX: Hack - can't pass mlist entries into _find_mlist(). bool mlist_full_info; std::vector mlist; static void _fill_monster_list(bool full_info) { std::vector temp; get_monster_info(temp); mlist_full_info = full_info; // Get the unique entries. mlist.clear(); unsigned int start = 0, end = 1; while (start < temp.size()) { mlist.push_back(temp[start]); for (end = start + 1; end < temp.size(); ++end) { if (monster_info::less_than(temp[start], temp[end], full_info)) { break; } } start = end; } } static int _mlist_letter_to_index(char idx) { if (idx >= 'b') idx--; if (idx >= 'h') idx--; if (idx >= 'j') idx--; if (idx >= 'k') idx--; if (idx >= 'l') idx--; return (idx - 'a'); } #endif range_view_annotator::range_view_annotator(int range) { if (Options.darken_beyond_range && range >= 0) { crawl_state.darken_range = range; viewwindow(false, false); } } range_view_annotator::~range_view_annotator() { if (Options.darken_beyond_range && crawl_state.darken_range >= 0) { crawl_state.darken_range = -1; viewwindow(false, false); } } bool _dist_ok(const dist& moves, int range, targ_mode_type mode, bool may_target_self, bool cancel_at_self) { if (!moves.isCancel && moves.isTarget) { if (!you.see_cell(moves.target)) { mpr("Sorry, you can't target what you can't see.", MSGCH_EXAMINE_FILTER); return (false); } if (moves.target == you.pos()) { // may_target_self == makes (some) sense to target yourself // (SPFLAG_AREA) // cancel_at_self == not allowed to target yourself // (SPFLAG_NOT_SELF) if (cancel_at_self) { mpr("Sorry, you can't target yourself.", MSGCH_EXAMINE_FILTER); return (false); } if (!may_target_self && (mode == TARG_ENEMY || mode == TARG_HOSTILE)) { if (Options.allow_self_target == CONFIRM_CANCEL) { mpr("That would be overly suicidal.", MSGCH_EXAMINE_FILTER); return (false); } else if (Options.allow_self_target == CONFIRM_PROMPT) { return yesno("Really target yourself?", false, 'n'); } } } // Check range if (range >= 0 && grid_distance(moves.target, you.pos()) > range) { mpr("That is beyond the maximum range.", MSGCH_EXAMINE_FILTER); return (false); } } // Some odd cases if (!moves.isValid && !moves.isCancel) return yesno("Are you sure you want to fizzle?", false, 'n'); return (true); } // Assuming the target is in view, is line-of-fire // blocked, and by what? static bool _blocked_ray(const coord_def &where, dungeon_feature_type* feat = NULL) { if (exists_ray(you.pos(), where)) return (false); if (feat == NULL) return (true); *feat = ray_blocker(you.pos(), where); return (true); } std::string _targ_mode_name(targ_mode_type mode) { switch (mode) { case TARG_ANY: return ("any"); case TARG_ENEMY: return ("enemies"); case TARG_FRIEND: return ("friends"); case TARG_HOSTILE: return ("hostiles"); default: return ("buggy"); } } #ifndef USE_TILE bool _init_mlist() { const int full_info = update_monster_pane(); if (full_info != -1) { _fill_monster_list(full_info); return (true); } else return (false); } #endif // Find a good square to start targetting from. static coord_def _find_default_target(targetting_type restricts, targ_mode_type mode, int range, bool needs_path) { coord_def result = you.pos(); bool success = false; if (restricts == DIR_TARGET_OBJECT) { // Try to find an object. success = _find_square_wrapper(result, 1, _find_object, needs_path, TARG_ANY, range, true, LOS_FLIPVH); } else if (mode == TARG_ENEMY || mode == TARG_HOSTILE) { // Try to find an enemy monster. // First try to pick our previous target. const monsters *mon_target = get_current_target(); if (mon_target // not made friendly since then && (mons_attitude(mon_target) == ATT_HOSTILE || mode == TARG_ENEMY && !mon_target->friendly()) // still in range && _is_target_in_range(mon_target->pos(), range)) { result = mon_target->pos(); success = true; } else { // The previous target is no good. Try to find one from scratch. success = _find_square_wrapper(result, 1, _find_monster, needs_path, mode, range, true); // If we couldn't, maybe it was because of line-of-fire issues. // Check if that's happening, and inform the user (because it's // pretty confusing.) if (!success && needs_path && _find_square_wrapper(result, 1, _find_monster, false, mode, range, true)) { mpr("All monsters which could be auto-targeted are covered by " "a wall or statue which interrupts your line of fire, even " "though it doesn't interrupt your line of sight.", MSGCH_PROMPT); } } } if (!success) result = you.pos(); return result; } static void _draw_beam(ray_def ray, const coord_def& beam_target, int range) { // Draw the new ray with magenta '*'s, not including your square // or the target square. Out-of-range cells get grey '*'s instead. while (ray.pos() != beam_target) { if (ray.pos() != you.pos()) { ASSERT(in_los(ray.pos())); const bool in_range = (range < 0) || grid_distance(ray.pos(), you.pos()) <= range; #ifdef USE_TILE tile_place_ray(ray.pos(), in_range); #else const int bcol = in_range ? MAGENTA : DARKGREY; _draw_ray_glyph(ray.pos(), bcol, '*', bcol | COLFLAG_REVERSE, in_range); #endif } ray.advance(); } textcolor(LIGHTGREY); #ifdef USE_TILE const bool in_range = (range < 0 || grid_distance(ray.pos(), you.pos()) <= range); tile_place_ray(beam_target, in_range); #endif } void direction(dist& moves, const targetting_type restricts, targ_mode_type mode, const int range, const bool just_looking, const bool needs_path, const bool may_target_monster, const bool may_target_self, const char *prompt, targetting_behaviour *beh, const bool cancel_at_self) { if (!beh) { static targetting_behaviour stock_behaviour; beh = &stock_behaviour; } beh->just_looking = just_looking; #ifndef USE_TILE crawl_state.mlist_targetting = false; if (may_target_monster && restricts != DIR_DIR && Options.mlist_targetting) { crawl_state.mlist_targetting = _init_mlist(); } #endif if (crawl_state.is_replaying_keys() && restricts != DIR_DIR) { _direction_again(moves, restricts, mode, range, just_looking, prompt, beh); return; } // NOTE: Even if just_looking is set, moves is still interesting, // because we can travel there! if (restricts == DIR_DIR) { direction_choose_compass(moves, beh); return; } cursor_control ccon(!Options.use_fake_cursor); mouse_control mc(needs_path && !just_looking ? MOUSE_MODE_TARGET_PATH : MOUSE_MODE_TARGET); range_view_annotator rva(range); int dir = 0; bool show_beam = Options.show_beam && !just_looking && needs_path; ray_def ray; // The possibly filled in beam. bool have_beam = false; // Whether it is filled in. coord_def beam_target; // What target is was chosen for. coord_def objfind_pos, monsfind_pos; // init moves.delta.reset(); moves.target = objfind_pos = monsfind_pos = you.pos(); // Find a default target. if (Options.default_target) moves.target = _find_default_target(restricts, mode, range, needs_path); // If requested, show the beam on startup. if (show_beam) { have_beam = find_ray(you.pos(), moves.target, ray); beam_target = objfind_pos = monsfind_pos = moves.target; if (have_beam) { _draw_beam(ray, beam_target, range); #ifdef USE_TILE // In tiles, we need to refresh the window to get the beam drawn. viewwindow(false, true); #endif } } bool target_unshifted = Options.target_unshifted_dirs; bool show_prompt = true; bool moved_with_keys = true; while (true) { #if defined(USE_UNIX_SIGNALS) && defined(SIGHUP_SAVE) && defined(USE_CURSES) // If we've received a HUP signal then the user can't choose a // target. if (crawl_state.seen_hups) { moves.isValid = false; moves.isCancel = true; mpr("Targetting interrupted by HUP signal.", MSGCH_ERROR); return; } #endif bool allow_out_of_range = false; // Prompts might get scrolled off if you have too few lines available. // We'll live with that. if (!just_looking && (show_prompt || beh->should_redraw())) { _target_mode_prompt(prompt, restricts, moves.target); if ((mode == TARG_ANY || mode == TARG_FRIEND) && moves.target == you.pos()) { terse_describe_square(moves.target); } show_prompt = false; } // Reinit...this needs to be done every loop iteration // because moves is more persistent than loop_done. moves.isValid = false; moves.isTarget = false; moves.isMe = false; moves.isCancel = false; moves.isEndpoint = false; moves.choseRay = false; // This probably is called too often for Tiles. (jpeg) if (moved_with_keys) cursorxy(grid2viewX(moves.target.x), grid2viewY(moves.target.y)); command_type key_command = beh->get_command(); #ifdef USE_TILE // If a mouse command, update location to mouse position. if (key_command == CMD_TARGET_MOUSE_MOVE || key_command == CMD_TARGET_MOUSE_SELECT) { moved_with_keys = false; const coord_def &gc = tiles.get_cursor(); if (gc != Region::NO_CURSOR) { if (!map_bounds(gc)) continue; moves.target = gc; if (key_command == CMD_TARGET_MOUSE_SELECT) key_command = CMD_TARGET_SELECT; } else key_command = CMD_NO_CMD; } else moved_with_keys = true; #endif if (target_unshifted && moves.target == you.pos() && restricts != DIR_TARGET) { key_command = shift_direction(key_command); } if (target_unshifted && (key_command == CMD_TARGET_CYCLE_FORWARD || key_command == CMD_TARGET_CYCLE_BACK || key_command == CMD_TARGET_OBJ_CYCLE_FORWARD || key_command == CMD_TARGET_OBJ_CYCLE_BACK)) { target_unshifted = false; } if (key_command == CMD_TARGET_MAYBE_PREV_TARGET) { if (moves.target == you.pos()) key_command = CMD_TARGET_PREV_TARGET; else key_command = CMD_TARGET_SELECT; } bool need_beam_redraw = false; bool force_redraw = false; bool loop_done = false; coord_def old_target = moves.target; int i; #ifndef USE_TILE if (key_command >= CMD_TARGET_CYCLE_MLIST && key_command <= CMD_TARGET_CYCLE_MLIST_END) { const int idx = _mlist_letter_to_index(key_command + 'a' - CMD_TARGET_CYCLE_MLIST); if (_find_square_wrapper(monsfind_pos, 1, _find_mlist, needs_path, idx, range, true)) { moves.target = monsfind_pos; } else { flush_input_buffer(FLUSH_ON_FAILURE); } } #endif switch (key_command) { // standard movement case CMD_TARGET_DOWN_LEFT: case CMD_TARGET_DOWN: case CMD_TARGET_DOWN_RIGHT: case CMD_TARGET_LEFT: case CMD_TARGET_RIGHT: case CMD_TARGET_UP_LEFT: case CMD_TARGET_UP: case CMD_TARGET_UP_RIGHT: i = _targetting_cmd_to_compass(key_command); moves.target += Compass[i]; break; case CMD_TARGET_DIR_DOWN_LEFT: case CMD_TARGET_DIR_DOWN: case CMD_TARGET_DIR_DOWN_RIGHT: case CMD_TARGET_DIR_LEFT: case CMD_TARGET_DIR_RIGHT: case CMD_TARGET_DIR_UP_LEFT: case CMD_TARGET_DIR_UP: case CMD_TARGET_DIR_UP_RIGHT: i = _targetting_cmd_to_compass(key_command); if (restricts != DIR_TARGET) { // A direction is allowed, and we've selected it. moves.delta = Compass[i]; // Needed for now...eventually shouldn't be necessary moves.target = you.pos() + moves.delta; moves.isValid = true; moves.isTarget = false; have_beam = false; show_beam = false; moves.choseRay = false; loop_done = true; } else { // Direction not allowed, so just move in that direction. // Maybe make this a bigger jump? moves.target += Compass[i] * 3; } break; case CMD_TARGET_SHOW_PROMPT: _target_mode_prompt(prompt, restricts, moves.target); break; #ifndef USE_TILE case CMD_TARGET_TOGGLE_MLIST: if (!crawl_state.mlist_targetting) crawl_state.mlist_targetting = _init_mlist(); else crawl_state.mlist_targetting = false; break; #endif #ifdef WIZARD case CMD_TARGET_CYCLE_BEAM: show_beam = true; have_beam = find_ray(you.pos(), moves.target, ray, opc_solid, BDS_DEFAULT, show_beam); beam_target = moves.target; need_beam_redraw = true; break; #endif case CMD_TARGET_TOGGLE_BEAM: if (show_beam) { show_beam = false; need_beam_redraw = true; } else { if (!needs_path) { mprf(MSGCH_EXAMINE_FILTER, "This spell doesn't need a beam path."); break; } have_beam = find_ray(you.pos(), moves.target, ray); beam_target = moves.target; show_beam = true; need_beam_redraw = true; } break; case CMD_TARGET_FIND_YOU: moves.target = you.pos(); moves.delta.reset(); break; case CMD_TARGET_FIND_TRAP: case CMD_TARGET_FIND_PORTAL: case CMD_TARGET_FIND_ALTAR: case CMD_TARGET_FIND_UPSTAIR: case CMD_TARGET_FIND_DOWNSTAIR: { const int thing_to_find = _targetting_cmd_to_feature(key_command); if (_find_square_wrapper(objfind_pos, 1, _find_feature, needs_path, thing_to_find, range, true, LOS_FLIPVH)) { moves.target = objfind_pos; } else { flush_input_buffer(FLUSH_ON_FAILURE); } break; } case CMD_TARGET_CYCLE_TARGET_MODE: mode = static_cast((mode + 1) % TARG_NUM_MODES); mprf("targetting mode is now: %s", _targ_mode_name(mode).c_str()); break; case CMD_TARGET_PREV_TARGET: // Do we have a previous target? if (you.prev_targ == MHITNOT || you.prev_targ == MHITYOU) { mpr("You haven't got a previous target.", MSGCH_EXAMINE_FILTER); break; } // We have a valid previous target (maybe). { const monsters *montarget = &menv[you.prev_targ]; if (!you.can_see(montarget)) { mpr("You can't see that creature any more.", MSGCH_EXAMINE_FILTER); } else { // We have all the information we need. moves.isValid = true; moves.isTarget = true; moves.target = montarget->pos(); if (!just_looking) { have_beam = false; loop_done = true; } } break; } // some modifiers to the basic selection command case CMD_TARGET_SELECT_FORCE: case CMD_TARGET_SELECT_ENDPOINT: case CMD_TARGET_SELECT_FORCE_ENDPOINT: if (key_command == CMD_TARGET_SELECT_ENDPOINT || key_command == CMD_TARGET_SELECT_FORCE_ENDPOINT) { moves.isEndpoint = true; } if (key_command == CMD_TARGET_SELECT_FORCE || key_command == CMD_TARGET_SELECT_FORCE_ENDPOINT) { allow_out_of_range = true; } // intentional fall-through case CMD_TARGET_SELECT: // finalise current choice if (!moves.isEndpoint) { const monsters* m = monster_at(moves.target); if (m && _mon_exposed(m)) moves.isEndpoint = true; } moves.isValid = true; moves.isTarget = true; loop_done = true; you.prev_grd_targ.reset(); // Maybe we should except just_looking here? if (const monsters* m = monster_at(moves.target)) you.prev_targ = m->mindex(); else if (moves.target == you.pos()) you.prev_targ = MHITYOU; else you.prev_grd_targ = moves.target; break; case CMD_TARGET_OBJ_CYCLE_BACK: case CMD_TARGET_OBJ_CYCLE_FORWARD: dir = (key_command == CMD_TARGET_OBJ_CYCLE_BACK) ? -1 : 1; if (_find_square_wrapper(objfind_pos, dir, _find_object, needs_path, TARG_ANY, range, true, (dir > 0 ? LOS_FLIPVH : LOS_FLIPHV))) { moves.target = objfind_pos; } else { flush_input_buffer(FLUSH_ON_FAILURE); } break; case CMD_TARGET_CYCLE_FORWARD: case CMD_TARGET_CYCLE_BACK: dir = (key_command == CMD_TARGET_CYCLE_BACK) ? -1 : 1; if (_find_square_wrapper(monsfind_pos, dir, _find_monster, needs_path, mode, range, true)) { moves.target = monsfind_pos; } else { flush_input_buffer(FLUSH_ON_FAILURE); } break; case CMD_TARGET_CANCEL: loop_done = true; moves.isCancel = true; beh->mark_ammo_nonchosen(); break; case CMD_TARGET_CENTER: moves.isValid = true; moves.isTarget = true; moves.target = you.pos(); break; #ifdef WIZARD case CMD_TARGET_WIZARD_MAKE_FRIENDLY: // Maybe we can skip this check...but it can't hurt if (!you.wizard || !in_bounds(moves.target)) break; { monsters* m = monster_at(moves.target); if (m == NULL) break; mon_attitude_type att = m->attitude; // During arena mode, skip directly from friendly to hostile. if (crawl_state.arena_suspended && att == ATT_FRIENDLY) att = ATT_NEUTRAL; switch (att) { case ATT_FRIENDLY: m->attitude = ATT_GOOD_NEUTRAL; m->flags &= ~MF_NO_REWARD; m->flags |= MF_WAS_NEUTRAL; break; case ATT_GOOD_NEUTRAL: m->attitude = ATT_STRICT_NEUTRAL; break; case ATT_STRICT_NEUTRAL: m->attitude = ATT_NEUTRAL; break; case ATT_NEUTRAL: m->attitude = ATT_HOSTILE; m->flags &= ~MF_WAS_NEUTRAL; break; case ATT_HOSTILE: m->attitude = ATT_FRIENDLY; m->flags |= MF_NO_REWARD; break; } // To update visual branding of friendlies. Only // seems capabable of adding bolding, not removing it, // though. viewwindow(false, true); } break; case CMD_TARGET_WIZARD_BLESS_MONSTER: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) wizard_apply_monster_blessing(m); break; case CMD_TARGET_WIZARD_MAKE_SHOUT: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) debug_make_monster_shout(m); break; case CMD_TARGET_WIZARD_GIVE_ITEM: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) wizard_give_monster_item(m); break; case CMD_TARGET_WIZARD_MOVE: if (!you.wizard || !in_bounds(moves.target)) break; wizard_move_player_or_monster(moves.target); loop_done = true; break; case CMD_TARGET_WIZARD_PATHFIND: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) debug_pathfind(m->mindex()); break; case CMD_TARGET_WIZARD_GAIN_LEVEL: break; case CMD_TARGET_WIZARD_MISCAST: if (!you.wizard || !in_bounds(moves.target)) break; if (you.pos() == moves.target) debug_miscast(NON_MONSTER); if (monsters* m = monster_at(moves.target)) debug_miscast(m->mindex()); break; case CMD_TARGET_WIZARD_MAKE_SUMMONED: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) wizard_make_monster_summoned(m); break; case CMD_TARGET_WIZARD_POLYMORPH: if (!you.wizard || !in_bounds(moves.target)) break; if (monsters* m = monster_at(moves.target)) wizard_polymorph_monster(m); break; case CMD_TARGET_WIZARD_DEBUG_MONSTER: if (!you.wizard || !in_bounds(moves.target)) break; if (monster_at(moves.target)) debug_stethoscope(mgrd(moves.target)); break; case CMD_TARGET_WIZARD_HURT_MONSTER: if (!you.wizard || !in_bounds(moves.target)) break; if (monster_at(moves.target)) { monster_at(moves.target)->hit_points = 1; mpr("Brought the mon down to near death."); } break; #endif case CMD_TARGET_DESCRIBE: full_describe_square(moves.target); force_redraw = true; if (crawl_state.arena_suspended) need_beam_redraw = true; break; case CMD_TARGET_HELP: show_targetting_help(); force_redraw = true; redraw_screen(); mesclr(true); show_prompt = true; break; default: break; } flush_prev_message(); if (loop_done) { // Confirm that the loop is really done. If it is, // break out. If not, just continue looping. if (just_looking) // easy out break; if (_dist_ok(moves, allow_out_of_range ? -1 : range, mode, may_target_self, cancel_at_self)) { // Finalise whatever is inside the loop // (moves-internal finalizations can be done later). moves.choseRay = have_beam; moves.ray = ray; break; } else show_prompt = true; } // We'll go on looping. Redraw whatever is necessary. if ( !in_viewport_bounds(grid2view(moves.target)) ) { // Tried to step out of bounds moves.target = old_target; } bool have_moved = (old_target != moves.target); if (beam_target != moves.target && show_beam) { have_beam = find_ray(you.pos(), moves.target, ray); beam_target = moves.target; need_beam_redraw = true; } if (have_moved || force_redraw) { mesclr(true); // Maybe not completely necessary. bool in_range = (range < 0 || grid_distance(moves.target,you.pos()) <= range); terse_describe_square(moves.target, in_range); flush_prev_message(); } #ifdef USE_TILE // Tiles always need a beam redraw if show_beam is true (and valid...) if (show_beam) need_beam_redraw = true; #endif if (need_beam_redraw) { #ifndef USE_TILE // Clear the old ray. viewwindow(false, false); #endif if (show_beam && have_beam) _draw_beam(ray, beam_target, range); #ifdef USE_TILE viewwindow(false, true); #endif } } moves.isMe = (moves.target == you.pos()); mesclr(); // We need this for directional explosions, otherwise they'll explode one // square away from the player. _extend_move_to_edge(moves); #ifdef USE_TILE tiles.place_cursor(CURSOR_MOUSE, Region::NO_CURSOR); #endif } std::string get_terse_square_desc(const coord_def &gc) { std::string desc = ""; const char *unseen_desc = "[unseen terrain]"; if (gc == you.pos()) desc = you.your_name; else if (!map_bounds(gc)) desc = unseen_desc; else if (!you.see_cell(gc)) { if (is_terrain_seen(gc)) { desc = "[" + feature_description(gc, false, DESC_PLAIN, false) + "]"; } else desc = unseen_desc; } else if (monster_at(gc) && you.can_see(monster_at(gc))) { const monsters& mons = *monster_at(gc); if (mons_is_mimic(mons.type) && !(mons.flags & MF_KNOWN_MIMIC)) desc = get_mimic_item(&mons).name(DESC_PLAIN); else desc = mons.full_name(DESC_PLAIN, true); } else if (you.visible_igrd(gc) != NON_ITEM) { if (mitm[you.visible_igrd(gc)].is_valid()) desc = mitm[you.visible_igrd(gc)].name(DESC_PLAIN); } else desc = feature_description(gc, false, DESC_PLAIN, false); return desc; } void terse_describe_square(const coord_def &c, bool in_range) { if (!you.see_cell(c)) _describe_oos_square(c); else if (in_bounds(c) ) _describe_cell(c, in_range); } void get_square_desc(const coord_def &c, describe_info &inf, bool examine_mons) { // NOTE: Keep this function in sync with full_describe_square. // Don't give out information for things outside LOS if (!you.see_cell(c)) return; const monsters* mons = monster_at(c); const int oid = you.visible_igrd(c); if (mons && mons->visible_to(&you)) { // First priority: monsters. if (examine_mons && !mons_is_unknown_mimic(mons)) { // If examine_mons is true (currently only for the Tiles // mouse-over information), set monster's // equipment/woundedness/enchantment description as title. std::string desc = get_monster_equipment_desc(mons) + ".\n"; std::string wounds = get_wounds_description(mons); if (!wounds.empty()) desc += mons->pronoun(PRONOUN_CAP) + wounds + "\n"; desc += _get_monster_desc(mons); inf.title = desc; } get_monster_db_desc(*mons, inf); } else if (oid != NON_ITEM) { // Second priority: objects. // If examine_mons is true, use terse descriptions. if (mitm[oid].is_valid()) get_item_desc(mitm[oid], inf, examine_mons); } else { // Third priority: features. get_feature_desc(c, inf); } } void full_describe_square(const coord_def &c) { // NOTE: Keep this function in sync with get_square_desc. // Don't give out information for things outside LOS if (!you.see_cell(c)) return; const monsters* mons = monster_at(c); const int oid = you.visible_igrd(c); if (mons && mons->visible_to(&you)) { // First priority: monsters. describe_monsters(*mons); } else if (oid != NON_ITEM) { // Second priority: objects. describe_item( mitm[oid] ); } else { // Third priority: features. describe_feature_wide(c); } redraw_screen(); mesclr(true); } static void _extend_move_to_edge(dist &moves) { if (moves.delta.origin()) return; // Now the tricky bit - extend the target x,y out to map edge. int mx = 0, my = 0; if (moves.delta.x > 0) mx = (GXM - 1) - you.pos().x; if (moves.delta.x < 0) mx = you.pos().x; if (moves.delta.y > 0) my = (GYM - 1) - you.pos().y; if (moves.delta.y < 0) my = you.pos().y; if (!(mx == 0 || my == 0)) { if (mx < my) my = mx; else mx = my; } moves.target.x = you.pos().x + moves.delta.x * mx; moves.target.y = you.pos().y + moves.delta.y * my; } // Attempts to describe a square that's not in line-of-sight. If // there's a stash on the square, announces the top item and number // of items, otherwise, if there's a stair that's in the travel // cache and noted in the Dungeon (O)verview, names the stair. static void _describe_oos_square(const coord_def& where) { mpr("You can't see that place.", MSGCH_EXAMINE_FILTER); if (!in_bounds(where) || !is_terrain_seen(where)) return; describe_stash(where.x, where.y); _describe_feature(where, true); } bool in_vlos(int x, int y) { return in_vlos(coord_def(x,y)); } bool in_vlos(const coord_def &pos) { return (in_los_bounds(pos) && (env.show(view2show(pos)) || pos == grid2view(you.pos()))); } bool in_los(int x, int y) { return in_los(coord_def(x,y)); } bool in_los(const coord_def& pos) { return (in_vlos(grid2view(pos))); } static bool _mons_is_valid_target(const monsters *mon, int mode, int range) { // Monster types that you can't gain experience from don't count as // monsters. if (mons_class_flag(mon->type, M_NO_EXP_GAIN)) return (false); // Unknown mimics don't count as monsters, either. if (mons_is_mimic(mon->type) && !(mon->flags & MF_KNOWN_MIMIC)) { return (false); } // Don't usually target unseen monsters... if (!mon->visible_to(&you)) { // ...unless it creates a "disturbance in the water". // Since you can't see the monster, assume it's not a friend. // Also, don't target submerged monsters if there are other // targets in sight. (This might be too restrictive.) return (mode != TARG_FRIEND && _mon_exposed(mon) && i_feel_safe(false, false, true, range)); } return (true); } #ifndef USE_TILE static bool _find_mlist(const coord_def& where, int idx, bool need_path, int range = -1) { if (static_cast(mlist.size()) <= idx) return (false); if (!_is_target_in_range(where, range) || !in_los(where)) return (false); const monsters* mon = monster_at(where); if (mon == NULL) return (false); int real_idx = 0; for (unsigned int i = 0; i+1 < mlist.size(); ++i) { if (real_idx == idx) { real_idx = i; break; } // While the monsters are identical, don't increase real_idx. if (!monster_info::less_than(mlist[i], mlist[i+1], mlist_full_info)) continue; real_idx++; } if (!_mons_is_valid_target(mon, TARG_ANY, range)) return (false); if (need_path && _blocked_ray(mon->pos())) return (false); const monsters *monl = mlist[real_idx].m_mon; extern mon_attitude_type mons_attitude(const monsters *m); if (mons_attitude(mon) != mlist[idx].m_attitude) return (false); if (mon->type != monl->type) return (mons_is_mimic(mon->type) && mons_is_mimic(monl->type)); if (mlist_full_info) { if (mons_is_zombified(mon)) // Both monsters are zombies. return (mon->base_monster == monl->base_monster); if (mon->has_hydra_multi_attack()) return (mon->number == monl->number); } if (mon->type == MONS_PLAYER_GHOST) return (mon->name(DESC_PLAIN) == monl->name(DESC_PLAIN)); // Else the two monsters are identical. return (true); } #endif static bool _find_monster( const coord_def& where, int mode, bool need_path, int range = -1) { #ifdef CLUA_BINDINGS { coord_def dp = grid2player(where); // We could pass more info here. maybe_bool x = clua.callmbooleanfn("ch_target_monster", "dd", dp.x, dp.y); if (x != B_MAYBE) return (tobool(x)); } #endif // Target the player for friendly and general spells. if ((mode == TARG_FRIEND || mode == TARG_ANY) && where == you.pos()) return (true); // Don't target out of range. if (!_is_target_in_range(where, range)) return (false); const monsters* mon = monster_at(where); // No monster or outside LOS. if (mon == NULL || !in_los(where)) return (false); // Monster in LOS but only via glass walls, so no direct path. if (need_path && !you.see_cell_no_trans(where)) return (false); if (!_mons_is_valid_target(mon, mode, range)) return (false); if (need_path && _blocked_ray(mon->pos())) return (false); // Now compare target modes. if (mode == TARG_ANY) return (true); if (mode == TARG_HOSTILE) return (mons_attitude(mon) == ATT_HOSTILE); if (mode == TARG_FRIEND) return (mon->friendly()); ASSERT(mode == TARG_ENEMY); if (mon->friendly()) return (false); // Don't target zero xp monsters. return (!mons_class_flag(mon->type, M_NO_EXP_GAIN)); } static bool _find_feature( const coord_def& where, int mode, bool /* need_path */, int /* range */) { // The stair need not be in LOS if the square is mapped. if (!in_los(where) && !is_terrain_seen(where)) return (false); return is_feature(mode, where); } static bool _find_object(const coord_def& where, int mode, bool need_path, int range) { // Don't target out of range. if (!_is_target_in_range(where, range)) return (false); if (need_path && (!you.see_cell(where) || _blocked_ray(where))) return (false); return (env.map_knowledge(where).item() != SHOW_ITEM_NONE); } static int _next_los(int dir, int los, bool wrap) { if (los == LOS_ANY) return (wrap? los : LOS_NONE); bool vis = los & LOS_VISIBLE; bool hidden = los & LOS_HIDDEN; bool flipvh = los & LOS_FLIPVH; bool fliphv = los & LOS_FLIPHV; if (!vis && !hidden) vis = true; if (wrap) { if (!flipvh && !fliphv) return (los); // We have to invert flipvh and fliphv if we're wrapping. Here's // why: // // * Say the cursor is on the last item in LOS, there are no // items outside LOS, and wrap == true. flipvh is true. // * We set wrap false and flip from visible to hidden, but there // are no hidden items. So now we need to flip back to visible // so we can go back to the first item in LOS. Unless we set // fliphv, we can't flip from hidden to visible. // los = flipvh? LOS_FLIPHV : LOS_FLIPVH; } else { if (!flipvh && !fliphv) return (LOS_NONE); if (flipvh && vis != (dir == 1)) return (LOS_NONE); if (fliphv && vis == (dir == 1)) return (LOS_NONE); } los = (los & ~LOS_VISMASK) | (vis? LOS_HIDDEN : LOS_VISIBLE); return (los); } bool in_viewport_bounds(int x, int y) { return crawl_view.in_view_viewport(coord_def(x, y)); } bool in_los_bounds(const coord_def& p) { return crawl_view.in_view_los(p); } //--------------------------------------------------------------- // // find_square // // Finds the next monster/object/whatever (moving in a spiral // outwards from the player, so closer targets are chosen first; // starts to player's left) and puts its coordinates in mfp. // Returns 1 if it found something, zero otherwise. If direction // is -1, goes backwards. // //--------------------------------------------------------------- static char _find_square(coord_def &mfp, int direction, bool (*find_targ)(const coord_def& wh, int mode, bool need_path, int range), bool need_path, int mode, int range, bool wrap, int los) { // the day will come when [unsigned] chars will be consigned to // the fires of Gehenna. Not quite yet, though. int temp_xps = mfp.x; int temp_yps = mfp.y; int x_change = 0; int y_change = 0; bool onlyVis = false, onlyHidden = false; int i, j; if (los == LOS_NONE) return (0); if (los == LOS_FLIPVH || los == LOS_FLIPHV) { if (in_los_bounds(mfp)) { // We've been told to flip between visible/hidden, so we // need to find what we're currently on. const bool vis = you.see_cell(view2grid(mfp)); if (wrap && (vis != (los == LOS_FLIPVH)) == (direction == 1)) { // We've already flipped over into the other direction, // so correct the flip direction if we're wrapping. los = (los == LOS_FLIPHV ? LOS_FLIPVH : LOS_FLIPHV); } los = (los & ~LOS_VISMASK) | (vis ? LOS_VISIBLE : LOS_HIDDEN); } else { if (wrap) los = LOS_HIDDEN | (direction > 0 ? LOS_FLIPHV : LOS_FLIPVH); else los |= LOS_HIDDEN; } } onlyVis = (los & LOS_VISIBLE); onlyHidden = (los & LOS_HIDDEN); int radius = 0; if (crawl_view.viewsz.x > crawl_view.viewsz.y) radius = crawl_view.viewsz.x - LOS_RADIUS - 1; else radius = crawl_view.viewsz.y - LOS_RADIUS - 1; const coord_def vyou = grid2view(you.pos()); const int minx = vyou.x - radius, maxx = vyou.x + radius, miny = vyou.y - radius, maxy = vyou.y + radius, ctrx = vyou.x, ctry = vyou.y; while (temp_xps >= minx - 1 && temp_xps <= maxx && temp_yps <= maxy && temp_yps >= miny - 1) { if (direction == 1 && temp_xps == minx && temp_yps == maxy) { mfp = vyou; if (find_targ(you.pos(), mode, need_path, range)) return (1); return (_find_square(mfp, direction, find_targ, need_path, mode, range, false, _next_los(direction, los, wrap))); } if (direction == -1 && temp_xps == ctrx && temp_yps == ctry) { mfp = coord_def(minx, maxy); return _find_square(mfp, direction, find_targ, need_path, mode, range, false, _next_los(direction, los, wrap)); } if (direction == 1) { if (temp_xps == minx - 1) { x_change = 0; y_change = -1; } else if (temp_xps == ctrx && temp_yps == ctry) { x_change = -1; y_change = 0; } else if (abs(temp_xps - ctrx) <= abs(temp_yps - ctry)) { if (temp_xps - ctrx >= 0 && temp_yps - ctry <= 0) { if (abs(temp_xps - ctrx) > abs(temp_yps - ctry + 1)) { x_change = 0; y_change = -1; if (temp_xps - ctrx > 0) y_change = 1; goto finished_spiralling; } } x_change = -1; if (temp_yps - ctry < 0) x_change = 1; y_change = 0; } else { x_change = 0; y_change = -1; if (temp_xps - ctrx > 0) y_change = 1; } } // end if (direction == 1) else { // This part checks all eight surrounding squares to find the // one that leads on to the present square. for (i = -1; i < 2; ++i) for (j = -1; j < 2; ++j) { if (i == 0 && j == 0) continue; if (temp_xps + i == minx - 1) { x_change = 0; y_change = -1; } else if (temp_xps + i - ctrx == 0 && temp_yps + j - ctry == 0) { x_change = -1; y_change = 0; } else if (abs(temp_xps + i - ctrx) <= abs(temp_yps + j - ctry)) { const int xi = temp_xps + i - ctrx; const int yj = temp_yps + j - ctry; if (xi >= 0 && yj <= 0 && abs(xi) > abs(yj + 1)) { x_change = 0; y_change = -1; if (xi > 0) y_change = 1; goto finished_spiralling; } x_change = -1; if (yj < 0) x_change = 1; y_change = 0; } else { x_change = 0; y_change = -1; if (temp_xps + i - ctrx > 0) y_change = 1; } if (temp_xps + i + x_change == temp_xps && temp_yps + j + y_change == temp_yps) { goto finished_spiralling; } } } // end else finished_spiralling: x_change *= direction; y_change *= direction; temp_xps += x_change; if (temp_yps + y_change <= maxy) // it can wrap, unfortunately temp_yps += y_change; const int targ_x = you.pos().x + temp_xps - ctrx; const int targ_y = you.pos().y + temp_yps - ctry; if (!crawl_view.in_grid_viewport(coord_def(targ_x, targ_y))) continue; if (!in_bounds(targ_x, targ_y)) continue; if ((onlyVis || onlyHidden) && onlyVis != in_los(targ_x, targ_y)) continue; if (find_targ(coord_def(targ_x, targ_y), mode, need_path, range)) { mfp.set(temp_xps, temp_yps); return (1); } } mfp = (direction > 0 ? coord_def(ctrx, ctry) : coord_def(minx, maxy)); return (_find_square(mfp, direction, find_targ, need_path, mode, range, false, _next_los(direction, los, wrap))); } // XXX Unbelievably hacky. And to think that my goal was to clean up the code. // Identical to find_square, except that mfp is in grid coordinates // rather than view coordinates. static char _find_square_wrapper(coord_def& mfp, char direction, bool (*find_targ)(const coord_def& where, int mode, bool need_path, int range), bool need_path, int mode, int range, bool wrap, int los ) { mfp = grid2view(mfp); const char r = _find_square(mfp, direction, find_targ, need_path, mode, range, wrap, los); mfp = view2grid(mfp); return r; } static void _describe_feature(const coord_def& where, bool oos) { if (oos && !is_terrain_seen(where)) return; dungeon_feature_type grid = grd(where); if (grid == DNGN_SECRET_DOOR) grid = grid_secret_door_appearance(where); std::string desc; desc = feature_description(grid); if (desc.length()) { if (oos) desc = "[" + desc + "]"; msg_channel_type channel = MSGCH_EXAMINE; if (oos || grid == DNGN_FLOOR) channel = MSGCH_EXAMINE_FILTER; mpr(desc.c_str(), channel); } } // Returns a vector of features matching the given pattern. std::vector features_by_desc(const base_pattern &pattern) { std::vector features; if (pattern.valid()) { for (int i = 0; i < NUM_FEATURES; ++i) { std::string fdesc = feature_description(static_cast(i)); if (fdesc.empty()) continue; if (pattern.matches( fdesc )) features.push_back( dungeon_feature_type(i) ); } } return (features); } void describe_floor() { dungeon_feature_type grid = grd(you.pos()); std::string prefix = "There is "; std::string feat; std::string suffix = " here."; switch (grid) { case DNGN_FLOOR: case DNGN_FLOOR_SPECIAL: return; case DNGN_ENTER_SHOP: prefix = "There is an entrance to "; break; default: break; } feat = feature_description(you.pos(), is_bloodcovered(you.pos()), DESC_NOCAP_A, false); if (feat.empty()) return; msg_channel_type channel = MSGCH_EXAMINE; // Water is not terribly important if you don't mind it. if (feat_is_water(grid) && player_likes_water()) channel = MSGCH_EXAMINE_FILTER; mpr((prefix + feat + suffix).c_str(), channel); if (grid == DNGN_ENTER_LABYRINTH && you.is_undead != US_UNDEAD) mpr("Beware, for starvation awaits!", MSGCH_EXAMINE); } std::string thing_do_grammar(description_level_type dtype, bool add_stop, bool force_article, std::string desc) { if (add_stop && (desc.empty() || desc[desc.length() - 1] != '.')) desc += "."; if (dtype == DESC_PLAIN || (!force_article && isupper(desc[0]))) { if (isupper(desc[0])) { switch (dtype) { case DESC_PLAIN: case DESC_NOCAP_THE: case DESC_NOCAP_A: desc[0] = tolower(desc[0]); break; default: break; } } return (desc); } switch (dtype) { case DESC_CAP_THE: return "The " + desc; case DESC_NOCAP_THE: return "the " + desc; case DESC_CAP_A: return article_a(desc, false); case DESC_NOCAP_A: return article_a(desc, true); case DESC_NONE: return (""); default: return (desc); } } std::string feature_description(dungeon_feature_type grid, trap_type trap, bool bloody, description_level_type dtype, bool add_stop, bool base_desc) { std::string desc = raw_feature_description(grid, trap, base_desc); if (bloody) desc += ", spattered with blood"; return thing_do_grammar(dtype, add_stop, feat_is_trap(grid), desc); } static std::string _base_feature_desc(dungeon_feature_type grid, trap_type trap) { if (feat_is_trap(grid) && trap != NUM_TRAPS) { switch (trap) { case TRAP_DART: return ("dart trap"); case TRAP_ARROW: return ("arrow trap"); case TRAP_NEEDLE: return ("needle trap"); case TRAP_BOLT: return ("bolt trap"); case TRAP_SPEAR: return ("spear trap"); case TRAP_AXE: return ("axe trap"); case TRAP_BLADE: return ("blade trap"); case TRAP_NET: return ("net trap"); case TRAP_ALARM: return ("alarm trap"); case TRAP_SHAFT: return ("shaft"); case TRAP_TELEPORT: return ("teleportation trap"); case TRAP_ZOT: return ("Zot trap"); default: error_message_to_player(); return ("undefined trap"); } } switch (grid) { case DNGN_STONE_WALL: return ("stone wall"); case DNGN_ROCK_WALL: case DNGN_SECRET_DOOR: if (you.level_type == LEVEL_PANDEMONIUM) return ("wall of the weird stuff which makes up Pandemonium"); else return ("rock wall"); case DNGN_PERMAROCK_WALL: return ("unnaturally hard rock wall"); case DNGN_OPEN_SEA: return ("open sea"); case DNGN_CLOSED_DOOR: return ("closed door"); case DNGN_DETECTED_SECRET_DOOR: return ("detected secret door"); case DNGN_METAL_WALL: return ("metal wall"); case DNGN_GREEN_CRYSTAL_WALL: return ("wall of green crystal"); case DNGN_CLEAR_ROCK_WALL: return ("translucent rock wall"); case DNGN_CLEAR_STONE_WALL: return ("translucent stone wall"); case DNGN_CLEAR_PERMAROCK_WALL: return ("translucent unnaturally hard rock wall"); case DNGN_TREES: return ("Trees"); case DNGN_ORCISH_IDOL: if (you.species == SP_HILL_ORC) return ("idol of Beogh"); else return ("orcish idol"); case DNGN_WAX_WALL: return ("wall of solid wax"); case DNGN_GRANITE_STATUE: return ("granite statue"); case DNGN_LAVA: return ("Some lava"); case DNGN_DEEP_WATER: return ("Some deep water"); case DNGN_SHALLOW_WATER: return ("Some shallow water"); case DNGN_UNDISCOVERED_TRAP: case DNGN_FLOOR: case DNGN_FLOOR_SPECIAL: return ("Floor"); case DNGN_OPEN_DOOR: return ("open door"); case DNGN_ESCAPE_HATCH_DOWN: return ("escape hatch in the floor"); case DNGN_ESCAPE_HATCH_UP: return ("escape hatch in the ceiling"); case DNGN_STONE_STAIRS_DOWN_I: case DNGN_STONE_STAIRS_DOWN_II: case DNGN_STONE_STAIRS_DOWN_III: return ("stone staircase leading down"); case DNGN_STONE_STAIRS_UP_I: case DNGN_STONE_STAIRS_UP_II: case DNGN_STONE_STAIRS_UP_III: if (player_in_hell()) return ("gateway back to the Vestibule of Hell"); return ("stone staircase leading up"); case DNGN_ENTER_HELL: return ("gateway to Hell"); case DNGN_EXIT_HELL: return ("gateway back into the Dungeon"); case DNGN_TRAP_MECHANICAL: return ("mechanical trap"); case DNGN_TRAP_MAGICAL: return ("magical trap"); case DNGN_TRAP_NATURAL: return ("natural trap"); case DNGN_ENTER_SHOP: return ("shop"); case DNGN_ABANDONED_SHOP: return ("abandoned shop"); case DNGN_ENTER_LABYRINTH: return ("labyrinth entrance"); case DNGN_ENTER_DIS: return ("gateway to the Iron City of Dis"); case DNGN_ENTER_GEHENNA: return ("gateway to Gehenna"); case DNGN_ENTER_COCYTUS: return ("gateway to the freezing wastes of Cocytus"); case DNGN_ENTER_TARTARUS: return ("gateway to the decaying netherworld of Tartarus"); case DNGN_ENTER_ABYSS: return ("one-way gate to the infinite horrors of the Abyss"); case DNGN_EXIT_ABYSS: return ("gateway leading out of the Abyss"); case DNGN_STONE_ARCH: return ("empty arch of ancient stone"); case DNGN_ENTER_PANDEMONIUM: return ("one-way gate leading to the halls of Pandemonium"); case DNGN_EXIT_PANDEMONIUM: return ("gate leading out of Pandemonium"); case DNGN_TRANSIT_PANDEMONIUM: return ("gate leading to another region of Pandemonium"); case DNGN_ENTER_ORCISH_MINES: return ("staircase to the Orcish Mines"); case DNGN_ENTER_HIVE: return ("staircase to the Hive"); case DNGN_ENTER_LAIR: return ("staircase to the Lair"); case DNGN_ENTER_SLIME_PITS: return ("staircase to the Slime Pits"); case DNGN_ENTER_VAULTS: return ("staircase to the Vaults"); case DNGN_ENTER_CRYPT: return ("staircase to the Crypt"); case DNGN_ENTER_HALL_OF_BLADES: return ("staircase to the Hall of Blades"); case DNGN_ENTER_ZOT: return ("gate to the Realm of Zot"); case DNGN_ENTER_TEMPLE: return ("staircase to the Ecumenical Temple"); case DNGN_ENTER_SNAKE_PIT: return ("staircase to the Snake Pit"); case DNGN_ENTER_ELVEN_HALLS: return ("staircase to the Elven Halls"); case DNGN_ENTER_TOMB: return ("staircase to the Tomb"); case DNGN_ENTER_SWAMP: return ("staircase to the Swamp"); case DNGN_ENTER_SHOALS: return ("staircase to the Shoals"); case DNGN_ENTER_PORTAL_VAULT: // The bazaar description should be set in the bazaar marker; this // is the description for a portal of unknown type. return ("gate leading to a distant place"); case DNGN_EXIT_PORTAL_VAULT: return ("gate leading back to the Dungeon"); case DNGN_RETURN_FROM_ORCISH_MINES: case DNGN_RETURN_FROM_HIVE: case DNGN_RETURN_FROM_LAIR: case DNGN_RETURN_FROM_VAULTS: case DNGN_RETURN_FROM_TEMPLE: return ("staircase back to the Dungeon"); case DNGN_RETURN_FROM_SLIME_PITS: case DNGN_RETURN_FROM_SNAKE_PIT: case DNGN_RETURN_FROM_SWAMP: case DNGN_RETURN_FROM_SHOALS: return ("staircase back to the Lair"); case DNGN_RETURN_FROM_CRYPT: case DNGN_RETURN_FROM_HALL_OF_BLADES: return ("staircase back to the Vaults"); case DNGN_RETURN_FROM_ELVEN_HALLS: return ("staircase back to the Mines"); case DNGN_RETURN_FROM_TOMB: return ("staircase back to the Crypt"); case DNGN_RETURN_FROM_ZOT: return ("gate leading back out of this place"); // altars case DNGN_ALTAR_ZIN: return ("glowing silver altar of Zin"); case DNGN_ALTAR_SHINING_ONE: return ("glowing golden altar of the Shining One"); case DNGN_ALTAR_KIKUBAAQUDGHA: return ("ancient bone altar of Kikubaaqudgha"); case DNGN_ALTAR_YREDELEMNUL: return ("basalt altar of Yredelemnul"); case DNGN_ALTAR_XOM: return ("shimmering altar of Xom"); case DNGN_ALTAR_VEHUMET: return ("shining altar of Vehumet"); case DNGN_ALTAR_OKAWARU: return ("iron altar of Okawaru"); case DNGN_ALTAR_MAKHLEB: return ("burning altar of Makhleb"); case DNGN_ALTAR_SIF_MUNA: return ("deep blue altar of Sif Muna"); case DNGN_ALTAR_TROG: return ("bloodstained altar of Trog"); case DNGN_ALTAR_NEMELEX_XOBEH: return ("sparkling altar of Nemelex Xobeh"); case DNGN_ALTAR_ELYVILON: return ("white marble altar of Elyvilon"); case DNGN_ALTAR_LUGONU: return ("corrupted altar of Lugonu"); case DNGN_ALTAR_BEOGH: return ("roughly hewn altar of Beogh"); case DNGN_ALTAR_JIYVA: return ("viscous altar of Jiyva"); case DNGN_ALTAR_FEDHAS: return ("blossoming altar of Fedhas"); case DNGN_ALTAR_CHEIBRIADOS: return ("snail-covered altar of Cheibriados"); case DNGN_FOUNTAIN_BLUE: return ("fountain of clear blue water"); case DNGN_FOUNTAIN_SPARKLING: return ("fountain of sparkling water"); case DNGN_FOUNTAIN_BLOOD: return ("fountain of blood"); case DNGN_DRY_FOUNTAIN_BLUE: case DNGN_DRY_FOUNTAIN_SPARKLING: case DNGN_DRY_FOUNTAIN_BLOOD: case DNGN_PERMADRY_FOUNTAIN: return ("dry fountain"); default: return (""); } } std::string raw_feature_description(dungeon_feature_type grid, trap_type trap, bool base_desc) { std::string base_str = _base_feature_desc(grid, trap); if (base_desc) return (base_str); if (you.level_type == LEVEL_DUNGEON) { switch (you.where_are_you) { case BRANCH_SLIME_PITS: if (grid == DNGN_ROCK_WALL) base_str = "slime covered rock wall"; break; default: break; } } desc_map::iterator i = base_desc_to_short.find(base_str); if (i != base_desc_to_short.end()) return (i->second); return (base_str); } void set_feature_desc_short(dungeon_feature_type grid, const std::string &desc) { set_feature_desc_short(_base_feature_desc(grid, NUM_TRAPS), desc); } void set_feature_desc_short(const std::string &base_name, const std::string &_desc) { ASSERT(!base_name.empty()); CrawlHashTable &props = env.properties; if (!props.exists(SHORT_DESC_KEY)) props[SHORT_DESC_KEY].new_table(); CrawlHashTable &desc_table = props[SHORT_DESC_KEY].get_table(); if (_desc.empty()) { base_desc_to_short.erase(base_name); desc_table.erase(base_name); } else { std::string desc = replace_all(_desc, "$BASE", base_name); base_desc_to_short[base_name] = desc; desc_table[base_name] = desc; } } void setup_feature_descs_short() { base_desc_to_short.clear(); const CrawlHashTable &props = env.properties; if (!props.exists(SHORT_DESC_KEY)) return; const CrawlHashTable &desc_table = props[SHORT_DESC_KEY].get_table(); CrawlHashTable::const_iterator i; for (i = desc_table.begin(); i != desc_table.end(); ++i) base_desc_to_short[i->first] = i->second.get_string(); } #ifndef DEBUG_DIAGNOSTICS // Is a feature interesting enough to 'v'iew it, even if a player normally // doesn't care about descriptions, i.e. does the description hold important // information? (Yes, this is entirely subjective. --jpeg) static bool _interesting_feature(dungeon_feature_type feat) { return (get_feature_def(feat).flags & FFT_EXAMINE_HINT); } #endif std::string feature_description(const coord_def& where, bool bloody, description_level_type dtype, bool add_stop, bool base_desc) { std::string marker_desc = env.markers.property_at(where, MAT_ANY, "feature_description"); if (!marker_desc.empty()) { if (bloody) marker_desc += ", spattered with blood"; return thing_do_grammar(dtype, add_stop, false, marker_desc); } dungeon_feature_type grid = grd(where); if (grid == DNGN_SECRET_DOOR) grid = grid_secret_door_appearance(where); if (grid == DNGN_OPEN_DOOR || feat_is_closed_door(grid)) { const std::string door_desc_prefix = env.markers.property_at(where, MAT_ANY, "door_description_prefix"); const std::string door_desc_suffix = env.markers.property_at(where, MAT_ANY, "door_description_suffix"); const std::string door_desc_noun = env.markers.property_at(where, MAT_ANY, "door_description_noun"); const std::string door_desc_adj = env.markers.property_at(where, MAT_ANY, "door_description_adjective"); const std::string door_desc_veto = env.markers.property_at(where, MAT_ANY, "door_description_veto"); std::set all_door; find_connected_identical(where, grd(where), all_door); const char *adj, *noun; get_door_description(all_door.size(), &adj, &noun); std::string desc; if (!door_desc_adj.empty()) desc += door_desc_adj; else desc += adj; if (door_desc_veto.empty() || door_desc_veto != "veto") { desc += (grid == DNGN_OPEN_DOOR) ? "open " : "closed "; if (grid == DNGN_DETECTED_SECRET_DOOR) desc += "detected secret "; } desc += door_desc_prefix; if (!door_desc_noun.empty()) desc += door_desc_noun; else desc += noun; desc += door_desc_suffix; if (bloody) desc += ", spattered with blood"; return thing_do_grammar(dtype, add_stop, false, desc); } if (grid == DNGN_OPEN_SEA) { switch (dtype) { case DESC_CAP_A: dtype = DESC_CAP_THE; break; case DESC_NOCAP_A: dtype = DESC_NOCAP_THE; break; default: break; } } switch (grid) { case DNGN_TRAP_MECHANICAL: case DNGN_TRAP_MAGICAL: case DNGN_TRAP_NATURAL: return (feature_description(grid, get_trap_type(where), bloody, dtype, add_stop, base_desc)); case DNGN_ABANDONED_SHOP: return thing_do_grammar(dtype, add_stop, false, "An abandoned shop"); case DNGN_ENTER_SHOP: return shop_name(where, add_stop); case DNGN_ENTER_PORTAL_VAULT: // Should have been handled at the top of the function. return (thing_do_grammar( dtype, add_stop, false, "UNAMED PORTAL VAULT ENTRY")); default: return (feature_description(grid, NUM_TRAPS, bloody, dtype, add_stop, base_desc)); } } static std::string _describe_mons_enchantment(const monsters &mons, const mon_enchant &ench, bool paralysed) { // Suppress silly-looking combinations, even if they're // internally valid. if (paralysed && (ench.ench == ENCH_SLOW || ench.ench == ENCH_HASTE || ench.ench == ENCH_SWIFT || ench.ench == ENCH_PETRIFIED || ench.ench == ENCH_PETRIFYING)) { return ""; } if ((ench.ench == ENCH_HASTE || ench.ench == ENCH_BATTLE_FRENZY || ench.ench == ENCH_MIGHT) && mons.berserk()) { return ""; } if (ench.ench == ENCH_HASTE && mons.has_ench(ENCH_SLOW)) return ""; if (ench.ench == ENCH_SLOW && mons.has_ench(ENCH_HASTE)) return ""; if (ench.ench == ENCH_PETRIFIED && mons.has_ench(ENCH_PETRIFYING)) return ""; switch (ench.ench) { case ENCH_POISON: return "poisoned"; case ENCH_SICK: return "sick"; case ENCH_ROT: return "rotting away"; //jmf: "covered in sores"? case ENCH_CORONA: return "softly glowing"; case ENCH_SLOW: return "moving slowly"; case ENCH_INSANE: return "frenzied and insane"; case ENCH_BERSERK: return "berserk"; case ENCH_BATTLE_FRENZY: return "consumed by blood-lust"; case ENCH_HASTE: return "moving very quickly"; case ENCH_MIGHT: return "unusually strong"; case ENCH_CONFUSION: return "bewildered and confused"; case ENCH_INVIS: return "slightly transparent"; case ENCH_CHARM: return "in your thrall"; case ENCH_STICKY_FLAME: return "covered in liquid flames"; case ENCH_HELD: return "entangled in a net"; case ENCH_PETRIFIED: return "petrified"; case ENCH_PETRIFYING: return "slowly petrifying"; case ENCH_LOWERED_MR: return "susceptible to magic"; case ENCH_SWIFT: return "moving somewhat quickly"; default: return ""; } } static std::string _describe_monster_weapon(const monsters *mons) { std::string desc = ""; std::string name1, name2; const item_def *weap = mons->mslot_item(MSLOT_WEAPON); const item_def *alt = mons->mslot_item(MSLOT_ALT_WEAPON); if (weap) { name1 = weap->name(DESC_NOCAP_A, false, false, true, false, ISFLAG_KNOW_CURSE); } if (alt && mons_wields_two_weapons(mons)) { name2 = alt->name(DESC_NOCAP_A, false, false, true, false, ISFLAG_KNOW_CURSE); } if (name1.empty() && !name2.empty()) name1.swap(name2); if (name1 == name2 && weap) { item_def dup = *weap; ++dup.quantity; name1 = dup.name(DESC_NOCAP_A, false, false, true, true, ISFLAG_KNOW_CURSE); name2.clear(); } if (name1.empty()) return (desc); desc += " wielding "; desc += name1; if (!name2.empty()) { desc += " and "; desc += name2; } return (desc); } #ifdef DEBUG_DIAGNOSTICS static std::string _stair_destination_description(const coord_def &pos) { if (LevelInfo *linf = travel_cache.find_level_info(level_id::current())) { const stair_info *si = linf->get_stair(pos); if (si) return (" " + si->describe()); else if (feat_is_stair(grd(pos))) return (" (unknown stair)"); } return (""); } #endif std::string _mon_enchantments_string(const monsters* mon) { const bool paralysed = mon->paralysed(); std::vector enchant_descriptors; for (mon_enchant_list::const_iterator e = mon->enchantments.begin(); e != mon->enchantments.end(); ++e) { const std::string tmp = _describe_mons_enchantment(*mon, e->second, paralysed); if (!tmp.empty()) enchant_descriptors.push_back(tmp); } if (paralysed) enchant_descriptors.push_back("paralysed"); if (!enchant_descriptors.empty()) { return mon->pronoun(PRONOUN_CAP) + " is " + comma_separated_line(enchant_descriptors.begin(), enchant_descriptors.end()) + "."; } else return ""; } // Returns the description string for a given monster, including attitude // and enchantments but not equipment or wounds. static std::string _get_monster_desc(const monsters *mon) { std::string text = ""; std::string pronoun = mon->pronoun(PRONOUN_CAP); if (you.beheld_by(mon)) text += "You are mesmerised by her song.\n"; if (!mons_is_mimic(mon->type) && mons_behaviour_perceptible(mon)) { if (mon->asleep()) { text += pronoun + " appears to be " + (mons_is_confused(mon, true) ? "sleepwalking" : "resting") + ".\n"; } // Applies to both friendlies and hostiles else if (mons_is_fleeing(mon)) text += pronoun + " is retreating.\n"; // hostile with target != you else if (!mon->friendly() && !mon->neutral() && mon->foe != MHITYOU && !crawl_state.arena_suspended) { // Special case: batty monsters get set to BEH_WANDER as // part of their special behaviour. if (!mons_is_batty(mon)) text += pronoun + " doesn't appear to have noticed you.\n"; } } if (mon->attitude == ATT_FRIENDLY) text += pronoun + " is friendly.\n"; else if (mon->good_neutral()) text += pronoun + " seems to be peaceful towards you.\n"; else if (mon->neutral()) // don't differentiate between permanent or not text += pronoun + " is indifferent to you.\n"; if (mon->is_summoned() && (mon->type != MONS_RAKSHASA_FAKE && mon->type != MONS_MARA_FAKE)) { text += pronoun + " has been summoned.\n"; } if (mon->haloed()) text += pronoun + " is illuminated by a divine halo.\n"; if (mons_intel(mon) <= I_PLANT && mon->type != MONS_RAKSHASA_FAKE) text += pronoun + " is mindless.\n"; if (mons_enslaved_body_and_soul(mon)) { text += mon->pronoun(PRONOUN_CAP_POSSESSIVE) + " soul is ripe for the taking.\n"; } else if (mons_enslaved_soul(mon)) text += pronoun + " is a disembodied soul.\n"; dungeon_feature_type blocking_feat; if (!crawl_state.arena_suspended && mon->pos() != you.pos() && _blocked_ray(mon->pos(), &blocking_feat)) { text += "Your line of fire to " + mon->pronoun(PRONOUN_OBJECTIVE) + " is blocked by " + feature_description(blocking_feat, NUM_TRAPS, false, DESC_NOCAP_A) + "\n"; } text += _mon_enchantments_string(mon); return text; } static void _describe_monster(const monsters *mon) { // First print type and equipment. std::string text = get_monster_equipment_desc(mon) + "."; print_formatted_paragraph(text, MSGCH_EXAMINE); print_wounds(mon); // Print the rest of the description. text = _get_monster_desc(mon); if (!text.empty()) print_formatted_paragraph(text, MSGCH_EXAMINE); } // This method is called in two cases: // a) Monsters coming into view: "An ogre comes into view. It is wielding ..." // b) Monster description via 'x': "An ogre, wielding a club, and wearing ..." std::string get_monster_equipment_desc(const monsters *mon, bool full_desc, description_level_type mondtype, bool print_attitude) { std::string desc = ""; if (mondtype != DESC_NONE) { if (print_attitude && mon->type == MONS_PLAYER_GHOST) desc = get_ghost_description(*mon); else desc = mon->full_name(mondtype); if (print_attitude) { std::string str = ""; if (mon->friendly()) str = "friendly"; else if (mon->neutral()) str = "neutral"; if (mon->is_summoned()) { if (!str.empty()) str += ", "; str += "summoned"; } if (mon->type == MONS_DANCING_WEAPON || mon->type == MONS_PANDEMONIUM_DEMON || mon->type == MONS_PLAYER_GHOST || mons_is_known_mimic(mon)) { if (!str.empty()) str += " "; if (mon->type == MONS_DANCING_WEAPON) str += "dancing weapon"; else if (mon->type == MONS_PANDEMONIUM_DEMON) str += "pandemonium demon"; else if (mon->type == MONS_PLAYER_GHOST) { if (mon->is_summoned()) str += "illusion"; else str += "ghost"; } else str += "mimic"; } if (!str.empty()) desc += " (" + str + ")"; } } std::string weap = ""; // We don't report rakshasa equipment in order not to give away the // true rakshasa when it summons. But Mara is fine, because his weapons // and armour are cloned with him. if (mon->type != MONS_DANCING_WEAPON && (mon->type != MONS_RAKSHASA || mon->friendly())) { weap = _describe_monster_weapon(mon); } if (!weap.empty()) { if (full_desc) desc += ","; desc += weap; } // Print the rest of the equipment only for full descriptions. if (full_desc && ((mon->type != MONS_RAKSHASA && mon->type != MONS_MARA && mon->type != MONS_MARA_FAKE) || mon->friendly())) { const int mon_arm = mon->inv[MSLOT_ARMOUR]; const int mon_shd = mon->inv[MSLOT_SHIELD]; const int mon_qvr = mon->inv[MSLOT_MISSILE]; const int mon_alt = mon->inv[MSLOT_ALT_WEAPON]; const bool need_quiver = (mon_qvr != NON_ITEM && mon->friendly()); const bool need_alt_wpn = (mon_alt != NON_ITEM && mon->friendly() && !mons_wields_two_weapons(mon)); bool found_sth = !weap.empty(); if (mon_arm != NON_ITEM) { desc += ", "; if (found_sth && mon_shd == NON_ITEM && !need_quiver && !need_alt_wpn) { desc += "and "; } desc += "wearing "; desc += mitm[mon_arm].name(DESC_NOCAP_A); if (!found_sth) found_sth = true; } if (mon_shd != NON_ITEM) { desc += ", "; if (found_sth && !need_quiver && !need_alt_wpn) desc += "and "; desc += "wearing "; desc += mitm[mon_shd].name(DESC_NOCAP_A); if (!found_sth) found_sth = true; } // For friendly monsters, also list quivered missiles // and alternate weapon. if (mon->friendly()) { if (mon_qvr != NON_ITEM) { desc += ", "; if (found_sth && !need_alt_wpn) desc += "and "; desc += "quivering "; desc += mitm[mon_qvr].name(DESC_NOCAP_A); if (!found_sth) found_sth = true; } if (need_alt_wpn) { desc += ", "; if (found_sth) desc += "and "; desc += "carrying "; desc += mitm[mon_alt].name(DESC_NOCAP_A); } } } return desc; } // Describe a cell, guaranteed to be in view. static void _describe_cell(const coord_def& where, bool in_range) { bool mimic_item = false; bool monster_described = false; bool cloud_described = false; bool item_described = false; if (where == you.pos() && !crawl_state.arena_suspended) mpr("You.", MSGCH_EXAMINE_FILTER); if (const monsters* mon = monster_at(where)) { if (_mon_submerged_in_water(mon)) { mpr("There is a strange disturbance in the water here.", MSGCH_EXAMINE_FILTER); } else if (_mon_exposed_in_cloud(mon)) { mpr("There is a strange disturbance in the cloud here.", MSGCH_EXAMINE_FILTER); } #if DEBUG_DIAGNOSTICS if (!mon->visible_to(&you)) mpr("There is a non-visible monster here.", MSGCH_DIAGNOSTICS); #else if (!mon->visible_to(&you)) goto look_clouds; #endif if (mons_is_mimic(mon->type)) { if (mons_is_known_mimic(mon)) _describe_monster(mon); else { std::string name = get_menu_colour_prefix_tags(get_mimic_item(mon), DESC_NOCAP_A); mprf(MSGCH_FLOOR_ITEMS, "You see %s here.", name.c_str()); } mimic_item = true; item_described = true; } else { _describe_monster(mon); if (!in_range) { mprf(MSGCH_EXAMINE_FILTER, "%s is out of range.", mon->pronoun(PRONOUN_CAP).c_str()); } monster_described = true; } #if DEBUG_DIAGNOSTICS debug_stethoscope(mgrd(where)); #endif if (Tutorial.tutorial_left && tutorial_monster_interesting(mon)) { std::string msg; #ifdef USE_TILE msg = "(Right-click for more information.)"; #else msg = "(Press v for more information.)"; #endif print_formatted_paragraph(msg); } } #if (!DEBUG_DIAGNOSTICS) // removing warning look_clouds: #endif if (is_sanctuary(where)) { mprf("This square lies inside a sanctuary%s.", silenced(where) ? ", and is shrouded in silence" : ""); } else if (silenced(where)) mpr("This square is shrouded in silence."); if (env.cgrid(where) != EMPTY_CLOUD) { const int cloud_inspected = env.cgrid(where); mprf(MSGCH_EXAMINE, "There is a cloud of %s here.", cloud_name(cloud_inspected).c_str()); cloud_described = true; } int targ_item = you.visible_igrd(where); if (targ_item != NON_ITEM) { // If a mimic is on this square, we pretend it's the first item - bwr if (mimic_item) mpr("There is something else lying underneath.", MSGCH_FLOOR_ITEMS); else { if (mitm[ targ_item ].base_type == OBJ_GOLD) mprf(MSGCH_FLOOR_ITEMS, "A pile of gold coins."); else { std::string name = get_menu_colour_prefix_tags(mitm[targ_item], DESC_NOCAP_A); mprf(MSGCH_FLOOR_ITEMS, "You see %s here.", name.c_str()); } if (mitm[ targ_item ].link != NON_ITEM) { mprf(MSGCH_FLOOR_ITEMS, "There is something else lying underneath."); } } item_described = true; } bool bloody = false; if (is_bloodcovered(where)) bloody = true; std::string feature_desc = feature_description(where, bloody); #ifdef DEBUG_DIAGNOSTICS std::string marker; if (map_marker *mark = env.markers.find(where, MAT_ANY)) { std::string desc = mark->debug_describe(); if (desc.empty()) desc = "?"; marker = " (" + desc + ")"; } const std::string traveldest = _stair_destination_description(where); std::string height_desc; if (env.heightmap.get()) height_desc = make_stringf(" (height: %d)", (*env.heightmap)(where)); const dungeon_feature_type feat = grd(where); mprf(MSGCH_DIAGNOSTICS, "(%d,%d): %s - %s (%d/%s)%s%s%s", where.x, where.y, stringize_glyph(get_screen_glyph(where)).c_str(), feature_desc.c_str(), feat, dungeon_feature_name(feat), marker.c_str(), traveldest.c_str(), height_desc.c_str()); #else if (Tutorial.tutorial_left && tutorial_pos_interesting(where.x, where.y)) { #ifdef USE_TILE feature_desc += " (Right-click for more information.)"; #else feature_desc += " (Press v for more information.)"; #endif print_formatted_paragraph(feature_desc); } else { const dungeon_feature_type feat = grd(where); if (_interesting_feature(feat)) { #ifdef USE_TILE feature_desc += " (Right-click for more information.)"; #else feature_desc += " (Press 'v' for more information.)"; #endif } // Suppress "Floor." if there's something on that square that we've // already described. if ((feat == DNGN_FLOOR || feat == DNGN_FLOOR_SPECIAL) && !bloody && (monster_described || item_described || cloud_described)) { return; } msg_channel_type channel = MSGCH_EXAMINE; if (feat == DNGN_FLOOR || feat == DNGN_FLOOR_SPECIAL || feat_is_water(feat)) { channel = MSGCH_EXAMINE_FILTER; } mpr(feature_desc.c_str(), channel); } #endif } /////////////////////////////////////////////////////////////////////////// // targetting_behaviour targetting_behaviour::targetting_behaviour(bool look_around) : just_looking(look_around), compass(false) { } targetting_behaviour::~targetting_behaviour() { } int targetting_behaviour::get_key() { if (!crawl_state.is_replaying_keys()) flush_input_buffer(FLUSH_BEFORE_COMMAND); return unmangle_direction_keys(getchm(KMC_TARGETTING), KMC_TARGETTING, false, false); } command_type targetting_behaviour::get_command(int key) { if (key == -1) key = get_key(); command_type cmd = key_to_command(key, KMC_TARGETTING); if (cmd >= CMD_MIN_TARGET && cmd < CMD_TARGET_CYCLE_TARGET_MODE) return (cmd); #ifndef USE_TILE // Overrides the movement keys while mlist_targetting is active. if (crawl_state.mlist_targetting && islower(key)) return static_cast (CMD_TARGET_CYCLE_MLIST + (key - 'a')); #endif // XXX: hack if (cmd == CMD_TARGET_SELECT && key == ' ' && just_looking) cmd = CMD_TARGET_CANCEL; return (cmd); } bool targetting_behaviour::should_redraw() { return (false); } void targetting_behaviour::mark_ammo_nonchosen() { // Nothing to be done. }