/*
* File: exclude.cc
* Summary: Code related to travel exclusions.
*/
#include "AppHdr.h"
#include "exclude.h"
#include <algorithm>
#include "cloud.h"
#include "coord.h"
#include "coordit.h"
#include "map_knowledge.h"
#include "mon-util.h"
#include "options.h"
#include "overmap.h"
#include "stuff.h"
#include "env.h"
#include "tags.h"
#include "terrain.h"
#include "travel.h"
#include "tutorial.h"
#include "view.h"
static bool _mon_needs_auto_exclude(const monsters *mon, bool sleepy = false)
{
if (mons_is_stationary(mon))
{
if (sleepy)
return (false);
// Don't give away mimics unless already known.
return (!mons_is_mimic(mon->type)
|| testbits(mon->flags, MF_KNOWN_MIMIC));
}
// Auto exclusion only makes sense if the monster is still asleep.
return (mon->asleep());
}
// Check whether a given monster is listed in the auto_exclude option.
bool need_auto_exclude(const monsters *mon, bool sleepy)
{
// This only works if the name is lowercased.
std::string name = mon->name(DESC_BASENAME,
mons_is_stationary(mon)
&& testbits(mon->flags, MF_SEEN));
lowercase(name);
for (unsigned i = 0; i < Options.auto_exclude.size(); ++i)
if (Options.auto_exclude[i].matches(name)
&& _mon_needs_auto_exclude(mon, sleepy)
&& mon->attitude == ATT_HOSTILE)
{
return (true);
}
return (false);
}
// If the monster is in the auto_exclude list, automatically set an
// exclusion.
void set_auto_exclude(const monsters *mon)
{
if (need_auto_exclude(mon) && !is_exclude_root(mon->pos()))
{
set_exclude(mon->pos(), LOS_RADIUS, true);
// FIXME: If this happens for several monsters in the same turn
// (as is possible for some vaults), this could be really
// annoying. (jpeg)
mprf(MSGCH_WARN,
"Marking area around %s as unsafe for travelling.",
mon->name(DESC_NOCAP_THE).c_str());
#ifdef USE_TILE
viewwindow(false);
#endif
learned_something_new(TUT_AUTO_EXCLUSION, mon->pos());
}
}
// Clear auto exclusion if the monster is killed or wakes up with the
// player in sight. If sleepy is true, stationary monsters are ignored.
void remove_auto_exclude(const monsters *mon, bool sleepy)
{
if (need_auto_exclude(mon, sleepy))
{
del_exclude(mon->pos());
#ifdef USE_TILE
viewwindow(false);
#endif
}
}
opacity_type _feat_opacity(dungeon_feature_type feat)
{
return (feat_is_opaque(feat) ? OPC_OPAQUE : OPC_CLEAR);
}
// A cell is considered clear unless the player knows it's
// opaque.
struct opacity_excl : opacity_func
{
CLONE(opacity_excl)
opacity_type operator()(const coord_def& p) const
{
if (!is_terrain_seen(p))
return OPC_CLEAR;
else if (!is_terrain_changed(p))
return _feat_opacity(env.grid(p));
else if (env.map_knowledge(p).object.cls == SH_FEATURE)
return _feat_opacity(env.map_knowledge(p).object.feat);
else
{
// If you have seen monsters, items or clouds there,
// it must have been passable.
return OPC_CLEAR;
}
}
};
static opacity_excl opc_excl;
// Note: circle_def(r, C_ROUND) gives a circle with square radius r*r+1;
// this doesn't work well for radius 0, but then we want to
// skip LOS calculation in that case anyway since it doesn't
// currently short-cut for small bounds. So radius 0, 1 are special-cased.
travel_exclude::travel_exclude(const coord_def &p, int r,
bool autoexcl, std::string dsc, bool vaultexcl)
: pos(p), radius(r),
los(los_def(p, opc_excl, circle_def(r, C_ROUND))),
uptodate(false), autoex(autoexcl), desc(dsc), vault(vaultexcl)
{
set_los();
}
// For exclude_map[p] = foo;
travel_exclude::travel_exclude()
: pos(-1, -1), radius(-1),
los(coord_def(-1, -1), opc_excl, circle_def(-1, C_ROUND)),
uptodate(false), autoex(false), desc(""), vault(false)
{
}
extern exclude_set curr_excludes; // in travel.cc
void travel_exclude::set_los()
{
uptodate = true;
if (radius > 1)
{
// Radius might have been changed, and this is cheap.
los.set_bounds(circle_def(radius, C_ROUND));
los.update();
}
}
bool travel_exclude::affects(const coord_def& p) const
{
if (!uptodate)
{
mprf(MSGCH_ERROR, "exclusion not up-to-date: e (%d,%d) p (%d,%d)",
pos.x, pos.y, p.x, p.y);
}
if (radius == 0)
return (p == pos);
else if (radius == 1)
return ((p - pos).rdist() <= 1);
else
return (los.see_cell(p));
}
bool travel_exclude::in_bounds(const coord_def &p) const
{
return (radius == 0 && p == pos
|| radius == 1 && (p - pos).rdist() <= 1
|| los.in_bounds(p));
}
/////////////////////////////////////////////////////////////////////////
exclude_set::exclude_set()
{
}
void exclude_set::clear()
{
exclude_roots.clear();
exclude_points.clear();
}
void exclude_set::erase(const coord_def &p)
{
exclude_set::iterator it = exclude_roots.find(p);
if (it == exclude_roots.end())
return;
travel_exclude old_ex = it->second;
exclude_roots.erase(it);
recompute_excluded_points();
}
void exclude_set::add_exclude(travel_exclude &ex)
{
add_exclude_points(ex);
exclude_roots[ex.pos] = ex;
}
void exclude_set::add_exclude(const coord_def &p, int radius,
bool autoexcl, std::string desc,
bool vaultexcl)
{
travel_exclude ex(p, radius, autoexcl, desc, vaultexcl);
add_exclude(ex);
}
void exclude_set::add_exclude_points(travel_exclude& ex)
{
if (ex.radius == 0)
{
exclude_points.insert(ex.pos);
return;
}
if (!ex.uptodate)
ex.set_los();
else
ex.los.update();
for (radius_iterator ri(ex.pos, ex.radius, C_ROUND); ri; ++ri)
if (ex.affects(*ri))
exclude_points.insert(*ri);
}
void exclude_set::update_excluded_points()
{
for (iterator it = exclude_roots.begin(); it != exclude_roots.end(); ++it)
{
travel_exclude &ex = it->second;
if (!ex.uptodate)
{
recompute_excluded_points();
return;
}
}
}
void exclude_set::recompute_excluded_points(bool recompute_los)
{
exclude_points.clear();
for (iterator it = exclude_roots.begin(); it != exclude_roots.end(); ++it)
{
travel_exclude &ex = it->second;
if (recompute_los)
ex.set_los();
add_exclude_points(ex);
}
}
bool exclude_set::is_excluded(const coord_def &p) const
{
return (exclude_points.find(p) != exclude_points.end());
}
bool exclude_set::is_exclude_root(const coord_def &p) const
{
return (exclude_roots.find(p) != exclude_roots.end());
}
travel_exclude* exclude_set::get_exclude_root(const coord_def &p)
{
exclude_set::iterator it = exclude_roots.find(p);
if (it != exclude_roots.end())
return (&(it->second));
return (false);
}
size_t exclude_set::size() const
{
return exclude_roots.size();
}
bool exclude_set::empty() const
{
return exclude_roots.empty();
}
exclude_set::const_iterator exclude_set::begin() const
{
return exclude_roots.begin();
}
exclude_set::const_iterator exclude_set::end() const
{
return exclude_roots.end();
}
exclude_set::iterator exclude_set::begin()
{
return exclude_roots.begin();
}
exclude_set::iterator exclude_set::end()
{
return exclude_roots.end();
}
/////////////////////////////////////////////////////////////////////////
void _mark_excludes_non_updated(const coord_def &p)
{
for (exclude_set::iterator it = curr_excludes.begin();
it != curr_excludes.end(); ++it)
{
travel_exclude &ex = it->second;
ex.uptodate = ex.uptodate && ex.in_bounds(p);
}
}
void init_exclusion_los()
{
curr_excludes.recompute_excluded_points(true);
}
/*
* Update exclusions' LOS to reflect changes within their range.
* "changed" is a list of coordinates that have been changed.
* Only exclusions that might have one of the changed points
* in view are updated.
*/
void update_exclusion_los(std::vector<coord_def> changed)
{
if (changed.empty())
return;
for (unsigned int i = 0; i < changed.size(); ++i)
_mark_excludes_non_updated(changed[i]);
curr_excludes.update_excluded_points();
}
bool is_excluded(const coord_def &p, const exclude_set &exc)
{
return exc.is_excluded(p);
}
bool is_exclude_root(const coord_def &p)
{
return (curr_excludes.get_exclude_root(p));
}
#ifdef USE_TILE
// update Gmap for squares surrounding exclude centre
static void _tile_exclude_gmap_update(const coord_def &p)
{
for (int x = -8; x <= 8; x++)
for (int y = -8; y <= 8; y++)
{
int px = p.x+x, py = p.y+y;
if (in_bounds(coord_def(px,py)))
{
tiles.update_minimap(px, py);
}
}
}
#endif
static void _exclude_update()
{
if (can_travel_interlevel())
{
LevelInfo &li = travel_cache.get_level_info(level_id::current());
li.update();
}
set_level_exclusion_annotation(curr_excludes.get_exclusion_desc());
}
static void _exclude_update(const coord_def &p)
{
#ifdef USE_TILE
_tile_exclude_gmap_update(p);
#endif
_exclude_update();
}
// Catch up exclude updates from set_exclude with defer_updates=true.
//
// Warning: For tiles, this assumes all changed exclude centres
// are still there, so this won't work as is for
// del_exclude.
void deferred_exclude_update()
{
_exclude_update();
#ifdef USE_TILE
exclude_set::iterator it;
for (it = curr_excludes.begin(); it != curr_excludes.end(); ++it)
_tile_exclude_gmap_update(it->second.pos);
#endif
}
void clear_excludes()
{
// Sanity checks
if (!player_in_mappable_area())
return;
#ifdef USE_TILE
exclude_set::iterator it;
//for (int i = curr_excludes.size()-1; i >= 0; i--)
for (it = curr_excludes.begin(); it != curr_excludes.end(); ++it)
_tile_exclude_gmap_update(it->second.pos);
#endif
curr_excludes.clear();
clear_level_exclusion_annotation();
_exclude_update();
}
// Cycles the radius of an exclusion, including "off" state;
// may start at 0 < radius < LOS_RADIUS, but won't cycle there.
void cycle_exclude_radius(const coord_def &p)
{
if (travel_exclude *exc = curr_excludes.get_exclude_root(p))
{
if (exc->radius == LOS_RADIUS)
set_exclude(p, 0);
else
del_exclude(p);
}
else
set_exclude(p, LOS_RADIUS);
}
// Remove a possible exclude.
void del_exclude(const coord_def &p)
{
curr_excludes.erase(p);
_exclude_update(p);
}
// Set or update an exclude.
void set_exclude(const coord_def &p, int radius, bool autoexcl, bool vaultexcl,
bool defer_updates)
{
// Sanity checks; excludes can be set in Pan and regular dungeon
// levels only.
if (!player_in_mappable_area())
return;
if (!in_bounds(p))
return;
if (travel_exclude *exc = curr_excludes.get_exclude_root(p))
{
if (exc->desc.empty() && defer_updates)
{
int cl = env.cgrid(p);
if (env.cgrid(p) != EMPTY_CLOUD)
exc->desc = cloud_name(cl) + " cloud";
}
else if (exc->radius == radius)
return;
exc->radius = radius;
exc->uptodate = false;
curr_excludes.recompute_excluded_points();
}
else
{
std::string desc = "";
if (!defer_updates)
{
// Don't list a monster in the exclusion annotation if the
// exclusion was triggered by e.g. the flamethrowers' lua check.
const monsters *m = monster_at(p);
if (m && (you.can_see(m) || mons_is_stationary(m)
&& testbits(m->flags, MF_SEEN)))
{
desc = mons_type_name(m->type, DESC_PLAIN);
}
else
{
// Maybe it's a staircase?
const dungeon_feature_type feat = env.map_knowledge(p).feat();
const command_type dir = feat_stair_direction(feat);
if (dir == CMD_GO_UPSTAIRS)
desc = "upstairs";
else if (dir == CMD_GO_DOWNSTAIRS)
desc = "downstairs";
}
}
else
{
int cl = env.cgrid(p);
if (env.cgrid(p) != EMPTY_CLOUD)
desc = cloud_name(cl) + " cloud";
}
curr_excludes.add_exclude(p, radius, autoexcl, desc, vaultexcl);
}
if (!defer_updates)
_exclude_update(p);
}
// If a cell that was placed automatically no longer contains the original
// monster (or it is invisible), remove the exclusion.
void maybe_remove_autoexclusion(const coord_def &p)
{
if (travel_exclude *exc = curr_excludes.get_exclude_root(p))
{
if (!exc->autoex)
return;
const monsters *m = monster_at(p);
if (!m || !you.can_see(m) || m->attitude != ATT_HOSTILE
|| strcmp(mons_type_name(m->type, DESC_PLAIN).c_str(),
exc->desc.c_str()) != 0)
{
del_exclude(p);
}
}
}
// Lists all exclusions on the current level.
std::string exclude_set::get_exclusion_desc()
{
std::vector<std::string> desc;
int count_other = 0;
for (exclmap::iterator it = exclude_roots.begin();
it != exclude_roots.end(); ++it)
{
travel_exclude &ex = it->second;
if (ex.desc != "")
desc.push_back(ex.desc);
else
count_other++;
}
if (desc.size() > 1)
{
// Combine identical descriptions.
std::sort(desc.begin(), desc.end());
std::vector<std::string> help = desc;
desc.clear();
std::string old_desc = "";
int count = 1;
for (unsigned int i = 0; i < help.size(); ++i)
{
std::string tmp = help[i];
if (i == 0)
old_desc = tmp;
else
{
if (strcmp(tmp.c_str(), old_desc.c_str()) == 0)
count++;
else
{
if (count == 1)
desc.push_back(old_desc);
else
{
snprintf(info, INFO_SIZE, "%d %s",
count, pluralise(old_desc).c_str());
desc.push_back(info);
count = 1;
}
old_desc = tmp;
}
}
}
if (count == 1)
desc.push_back(old_desc);
else
{
snprintf(info, INFO_SIZE, "%d %s",
count, pluralise(old_desc).c_str());
desc.push_back(info);
}
}
if (count_other > 0)
{
snprintf(info, INFO_SIZE, "%d %sexclusion%s",
count_other, desc.empty() ? "" : "more ",
count_other > 1 ? "s" : "");
desc.push_back(info);
}
else if (desc.empty())
return "";
std::string desc_str = "";
if (desc.size() > 1 || count_other == 0)
{
snprintf(info, INFO_SIZE, "exclusion%s: ",
desc.size() > 1 ? "s" : "");
desc_str += info;
}
return (desc_str + comma_separated_line(desc.begin(), desc.end(),
" and ", ", "));
}
void marshallExcludes(writer& outf, const exclude_set& excludes)
{
marshallShort(outf, excludes.size());
exclude_set::const_iterator it;
if (excludes.size())
{
for (it = excludes.begin(); it != excludes.end(); ++it)
{
const travel_exclude &ex = it->second;
marshallCoord(outf, ex.pos);
marshallShort(outf, ex.radius);
marshallBoolean(outf, ex.autoex);
marshallString(outf, ex.desc);
// XXX: marshall travel_exclude::vault?
}
}
}
void unmarshallExcludes(reader& inf, char minorVersion, exclude_set &excludes)
{
excludes.clear();
int nexcludes = unmarshallShort(inf);
if (nexcludes)
{
for (int i = 0; i < nexcludes; ++i)
{
coord_def c;
unmarshallCoord(inf, c);
const int radius = unmarshallShort(inf);
const bool autoexcl = unmarshallBoolean(inf);
const std::string desc = unmarshallString(inf);
excludes.add_exclude(c, radius, autoexcl, desc);
}
}
}