/*
* File: direct.cc
* Summary: Functions used when picking squares.
* Written by: Linley Henzell
*/
#include "AppHdr.h"
#include "directn.h"
#include "format.h"
#include <cstdarg>
#include <sstream>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#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<std::string, std::string> 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<monster_info> list_mons;
std::vector<item_def> list_items;
std::vector<coord_def> 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<item_def> 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<monster_info>::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)
+ "</" + col_string + ">) ";
#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<formatted_string> 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<InvEntry*> 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 += "</" + colour_str +">) ";
#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<MenuEntry*> 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<view_desc_proc>(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<monster_info> mlist;
static void _fill_monster_list(bool full_info)
{
std::vector<monster_info> 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<targ_mode_type>((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<int>(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<dungeon_feature_type> features_by_desc(const base_pattern &pattern)
{
std::vector<dungeon_feature_type> features;
if (pattern.valid())
{
for (int i = 0; i < NUM_FEATURES; ++i)
{
std::string fdesc =
feature_description(static_cast<dungeon_feature_type>(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<coord_def> 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<std::string> 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 = "(<w>Right-click</w> for more information.)";
#else
msg = "(Press <w>v</w> 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 += " (<w>Right-click</w> for more information.)";
#else
feature_desc += " (Press <w>v</w> 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<command_type> (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.
}