/* * File: view.cc * Summary: Misc function used to render the dungeon. * Written by: Linley Henzell */ #include "AppHdr.h" #include "view.h" #include "shout.h" #include #include #include #include #include #include #include "externs.h" #include "map_knowledge.h" #include "viewchar.h" #include "viewgeom.h" #include "viewmap.h" #include "showsymb.h" #include "attitude-change.h" #include "branch.h" #include "cio.h" #include "cloud.h" #include "clua.h" #include "colour.h" #include "coord.h" #include "coordit.h" #include "database.h" #include "delay.h" #include "directn.h" #include "exclude.h" #include "feature.h" #include "files.h" #include "godabil.h" #include "macro.h" #include "message.h" #include "misc.h" #include "mon-behv.h" #include "mon-iter.h" #include "mon-stuff.h" #include "mon-util.h" #include "newgame.h" #include "options.h" #include "notes.h" #include "output.h" #include "overmap.h" #include "player.h" #include "random.h" #include "stuff.h" #include "env.h" #include "spells3.h" #include "stash.h" #include "tiles.h" #include "travel.h" #include "state.h" #include "terrain.h" #include "tilemcache.h" #include "tilesdl.h" #include "travel.h" #include "tutorial.h" #include "xom.h" #ifdef USE_TILE #include "tiledef-dngn.h" #endif #define DEBUG_PANE_BOUNDS 0 crawl_view_geometry crawl_view; bool is_notable_terrain(dungeon_feature_type ftype) { return (get_feature_def(ftype).is_notable()); } void handle_seen_interrupt(monsters* monster) { if (mons_is_unknown_mimic(monster)) return; activity_interrupt_data aid(monster); if (!monster->seen_context.empty()) aid.context = monster->seen_context; // XXX: Hack to make the 'seen' monster spec flag work. else if (testbits(monster->flags, MF_WAS_IN_VIEW) || testbits(monster->flags, MF_SEEN)) { aid.context = "already seen"; } else aid.context = "newly seen"; if (!mons_is_safe(monster) && !mons_class_flag(monster->type, M_NO_EXP_GAIN)) { interrupt_activity(AI_SEE_MONSTER, aid); } seen_monster( monster ); } void flush_comes_into_view() { if (!you.turn_is_over || (!you_are_delayed() && !crawl_state.is_repeating_cmd())) { return; } monsters* mon = crawl_state.which_mon_acting(); if (!mon || !mon->alive() || (mon->flags & MF_WAS_IN_VIEW) || !you.can_see(mon)) { return; } handle_seen_interrupt(mon); } void monster_grid_updates() { for (monster_iterator mi(&you.get_los()); mi; ++mi) { if ((mi->asleep() || mons_is_wandering(*mi)) && check_awaken(*mi)) { behaviour_event(*mi, ME_ALERT, MHITYOU, you.pos(), false); handle_monster_shouts(*mi); } if (!mi->visible_to(&you)) continue; good_god_follower_attitude_change(*mi); beogh_follower_convert(*mi); slime_convert(*mi); // XXX: Probably quite hackish. Allows for monsters going berserk when // they see the player. Currently only used for Duvessa, see the // function _elven_twin_dies in mon-stuff.cc. if (mi->flags & MF_GOING_BERSERK) { mi->flags &= ~MF_GOING_BERSERK; mi->go_berserk(true); } // XXX: Hack for triggering Dowan's spell changes. if (mi->props.exists("dowan_upgrade")) { mi->add_ench(ENCH_HASTE); mi->props.erase("dowan_upgrade"); simple_monster_message(*mi, " seems to find hidden reserves of power!"); } } } void update_monsters_in_view() { unsigned int num_hostile = 0; for (monster_iterator mi; mi; ++mi) { if (mons_near(*mi)) { if (mi->attitude == ATT_HOSTILE) num_hostile++; if (mons_is_unknown_mimic(*mi)) { // For unknown mimics, don't mark as seen, // but do mark it as in view for later messaging. // FIXME: is this correct? mi->flags |= MF_WAS_IN_VIEW; } else if (mi->visible_to(&you)) { handle_seen_interrupt(*mi); seen_monster(*mi); } else mi->flags &= ~MF_WAS_IN_VIEW; } else mi->flags &= ~MF_WAS_IN_VIEW; // If the monster hasn't been seen by the time that the player // gets control back then seen_context is out of date. mi->seen_context.clear(); } // Xom thinks it's hilarious the way the player picks up an ever // growing entourage of monsters while running through the Abyss. // To approximate this, if the number of hostile monsters in view // is greater than it ever was for this particular trip to the // Abyss, Xom is stimulated in proportion to the number of // hostile monsters. Thus if the entourage doesn't grow, then // Xom becomes bored. if (you.level_type == LEVEL_ABYSS && you.attribute[ATTR_ABYSS_ENTOURAGE] < num_hostile) { you.attribute[ATTR_ABYSS_ENTOURAGE] = num_hostile; xom_is_stimulated(16 * num_hostile); } } // We logically associate a difficulty parameter with each tile on each level, // to make deterministic magic mapping work. This function returns the // difficulty parameters for each tile on the current level, whose difficulty // is less than a certain amount. // // Random difficulties are used in the few cases where we want repeated maps // to give different results; scrolls and cards, since they are a finite // resource. static const FixedArray& _tile_difficulties(bool random) { // We will often be called with the same level parameter and cutoff, so // cache this (DS with passive mapping autoexploring could be 5000 calls // in a second or so). static FixedArray cache; static int cache_seed = -1; int seed = random ? -1 : (static_cast(you.where_are_you) << 8) + you.your_level - 1731813538; if (seed == cache_seed && !random) { return cache; } if (!random) { push_rng_state(); seed_rng(cache_seed); } cache_seed = seed; for (int y = Y_BOUND_1; y <= Y_BOUND_2; ++y) for (int x = X_BOUND_1; x <= X_BOUND_2; ++x) cache[x][y] = random2(100); if (!random) { pop_rng_state(); } return cache; } static std::auto_ptr > _tile_detectability() { std::auto_ptr > map(new FixedArray); std::vector flood_from; for (int x = X_BOUND_1; x <= X_BOUND_2; ++x) for (int y = Y_BOUND_1; y <= Y_BOUND_2; ++y) { (*map)(coord_def(x,y)) = false; if (feat_is_stair(grd[x][y])) { flood_from.push_back(coord_def(x, y)); } } flood_from.push_back(you.pos()); while (!flood_from.empty()) { coord_def p = flood_from.back(); flood_from.pop_back(); if (!in_bounds(p)) continue; if ((*map)(p)) continue; (*map)(p) = true; if (grd(p) < DNGN_MINSEE && grd(p) != DNGN_CLOSED_DOOR) continue; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) flood_from.push_back(p + coord_def(dx,dy)); } return map; } // Returns true if it succeeded. bool magic_mapping(int map_radius, int proportion, bool suppress_msg, bool force, bool deterministic, bool circular, coord_def pos) { if (!in_bounds(pos)) pos = you.pos(); if (!force && (testbits(env.level_flags, LFLAG_NO_MAGIC_MAP) || testbits(get_branch_flags(), BFLAG_NO_MAGIC_MAP))) { if (!suppress_msg) mpr("You feel momentarily disoriented."); return (false); } const bool wizard_map = (you.wizard && map_radius == 1000); if (!wizard_map) { if (map_radius > 50) map_radius = 50; else if (map_radius < 5) map_radius = 5; } // now gradually weaker with distance: const int pfar = (map_radius * 7) / 10; const int very_far = (map_radius * 9) / 10; bool did_map = false; int num_altars = 0; int num_shops_portals = 0; const FixedArray& difficulty = _tile_difficulties(!deterministic); std::auto_ptr > detectable; if (!deterministic) detectable = _tile_detectability(); for (radius_iterator ri(pos, map_radius, circular ? C_ROUND : C_SQUARE); ri; ++ri) { if (!wizard_map) { int threshold = proportion; const int dist = grid_distance( you.pos(), *ri ); if (dist > very_far) threshold = threshold / 3; else if (dist > pfar) threshold = threshold * 2 / 3; if (difficulty(*ri) > threshold) continue; } if (is_terrain_changed(*ri)) clear_map_knowledge_grid(*ri); if (!wizard_map && (is_terrain_seen(*ri) || is_terrain_mapped(*ri))) continue; if (!wizard_map && !deterministic && !((*detectable)(*ri))) continue; const dungeon_feature_type feat = grd(*ri); bool open = true; if (feat_is_solid(feat) && !feat_is_closed_door(feat)) { open = false; for (adjacent_iterator ai(*ri); ai; ++ai) { if (map_bounds(*ai) && (!feat_is_opaque(grd(*ai)) || feat_is_closed_door(grd(*ai)))) { open = true; break; } } } if (open) { if (wizard_map || !get_map_knowledge_obj(*ri)) set_map_knowledge_obj(*ri, grd(*ri)); if (wizard_map) { if (is_notable_terrain(feat)) seen_notable_thing(feat, *ri); set_terrain_seen(*ri); #ifdef USE_TILE // Can't use set_map_knowledge_obj because that would // overwrite the gmap. env.tile_bk_bg(*ri) = tile_idx_unseen_terrain(ri->x, ri->y, grd(*ri)); #endif } else { set_terrain_mapped(*ri); if (get_feature_dchar(feat) == DCHAR_ALTAR) num_altars++; else if (get_feature_dchar(feat) == DCHAR_ARCH) num_shops_portals++; } did_map = true; } } if (!suppress_msg) { mpr(did_map ? "You feel aware of your surroundings." : "You feel momentarily disoriented."); std::vector sensed; if (num_altars > 0) sensed.push_back(make_stringf("%d altar%s", num_altars, num_altars > 1 ? "s" : "")); if (num_shops_portals > 0) { const char* plur = num_shops_portals > 1 ? "s" : ""; sensed.push_back(make_stringf("%d shop%s/portal%s", num_shops_portals, plur, plur)); } if (!sensed.empty()) mpr_comma_separated_list("You sensed ", sensed); } return (did_map); } // Is the given monster near (in LOS of) the player? bool mons_near(const monsters *monster) { if (crawl_state.arena || crawl_state.arena_suspended) return (true); return (you.see_cell(monster->pos())); } bool mon_enemies_around(const monsters *monster) { // If the monster has a foe, return true. if (monster->foe != MHITNOT && monster->foe != MHITYOU) return (true); if (crawl_state.arena) { // If the arena-mode code in _handle_behaviour() hasn't set a foe then // we don't have one. return (false); } else if (monster->wont_attack()) { // Additionally, if an ally is nearby and *you* have a foe, // consider it as the ally's enemy too. return (mons_near(monster) && there_are_monsters_nearby(true)); } else { // For hostile monsters *you* are the main enemy. return (mons_near(monster)); } } // Returns a string containing an ASCII representation of the map. If fullscreen // is set to false, only the viewable area is returned. Leading and trailing // spaces are trimmed from each line. Leading and trailing empty lines are also // snipped. std::string screenshot(bool fullscreen) { UNUSED(fullscreen); // [ds] Screenshots need to be straight ASCII. We will now proceed to force // the char and feature tables back to ASCII. FixedVector char_table_bk; char_table_bk = Options.char_table; init_char_table(CSET_ASCII); init_show_table(); int firstnonspace = -1; int firstpopline = -1; int lastpopline = -1; std::vector lines(crawl_view.viewsz.y); for (int count_y = 1; count_y <= crawl_view.viewsz.y; count_y++) { int lastnonspace = -1; for (int count_x = 1; count_x <= crawl_view.viewsz.x; count_x++) { // in grid coords const coord_def gc = view2grid(coord_def(count_x, count_y)); int ch = (!map_bounds(gc)) ? 0 : (!crawl_view.in_grid_los(gc)) ? get_map_knowledge_char(gc) : (gc == you.pos()) ? you.symbol : get_screen_glyph(gc); if (ch && !isprint(ch)) { // [ds] Evil hack time again. Peek at grid, use that character. ch = get_feat_symbol(grid_appearance(gc)); } // More mangling to accommodate C strings. if (!ch) ch = ' '; if (ch != ' ') { lastnonspace = count_x; lastpopline = count_y; if (firstnonspace == -1 || firstnonspace > count_x) firstnonspace = count_x; if (firstpopline == -1) firstpopline = count_y; } lines[count_y - 1] += ch; } if (lastnonspace < (int) lines[count_y - 1].length()) lines[count_y - 1].erase(lastnonspace + 1); } // Restore char and feature tables. Options.char_table = char_table_bk; init_show_table(); std::ostringstream ss; if (firstpopline != -1 && lastpopline != -1) { if (firstnonspace == -1) firstnonspace = 0; for (int i = firstpopline; i <= lastpopline; ++i) { const std::string &ref = lines[i - 1]; if (firstnonspace < (int) ref.length()) ss << ref.substr(firstnonspace); ss << EOL; } } return (ss.str()); } static int _viewmap_flash_colour() { if (you.attribute[ATTR_SHADOWS]) return (DARKGREY); else if (you.berserk()) return (RED); return (BLACK); } // Updates one square of the view area. Should only be called for square // in LOS. void view_update_at(const coord_def &pos) { if (pos == you.pos()) return; const coord_def vp = grid2view(pos); const coord_def ep = view2show(vp); env.show.update_at(pos, ep); #ifndef USE_TILE show_type object = env.show(ep); if (!object) return; glyph g = get_show_glyph(object); int flash_colour = you.flash_colour; if (flash_colour == BLACK) flash_colour = _viewmap_flash_colour(); cgotoxy(vp.x, vp.y); put_colour_ch(flash_colour? real_colour(flash_colour) : g.col, g.ch); // Force colour back to normal, else clrscr() will flood screen // with this colour on DOS. textattr(LIGHTGREY); #endif } #ifndef USE_TILE void flash_monster_colour(const monsters *mon, unsigned char fmc_colour, int fmc_delay) { if (you.can_see(mon)) { unsigned char old_flash_colour = you.flash_colour; coord_def c(mon->pos()); you.flash_colour = fmc_colour; view_update_at(c); update_screen(); delay(fmc_delay); you.flash_colour = old_flash_colour; view_update_at(c); update_screen(); } } #endif bool view_update() { if (you.num_turns > you.last_view_update) { viewwindow(false); return (true); } return (false); } void flash_view(int colour) { you.flash_colour = colour; viewwindow(false, false); } void flash_view_delay(int colour, long flash_delay) { flash_view(colour); // Scale delay to match change in arena_delay. if (crawl_state.arena) { flash_delay *= Options.arena_delay; flash_delay /= 600; } delay(flash_delay); } static void _debug_pane_bounds() { #if DEBUG_PANE_BOUNDS // Doesn't work for HUD because print_stats() overwrites it. if (crawl_view.mlistsz.y > 0) { textcolor(WHITE); cgotoxy(1,1, GOTO_MLIST); cprintf("+ L"); cgotoxy(crawl_view.mlistsz.x-4, crawl_view.mlistsz.y, GOTO_MLIST); cprintf("L +"); } cgotoxy(1,1, GOTO_STAT); cprintf("+ H"); cgotoxy(crawl_view.hudsz.x-3, crawl_view.hudsz.y, GOTO_STAT); cprintf("H +"); cgotoxy(1,1, GOTO_MSG); cprintf("+ M"); cgotoxy(crawl_view.msgsz.x-2, crawl_view.msgsz.y, GOTO_MSG); cprintf("M +"); cgotoxy(crawl_view.viewp.x, crawl_view.viewp.y); cprintf("+V"); cgotoxy(crawl_view.viewp.x+crawl_view.viewsz.x-2, crawl_view.viewp.y+crawl_view.viewsz.y-1); cprintf("V+"); textcolor(LIGHTGREY); #endif } enum update_flags { UF_AFFECT_EXCLUDES = (1 << 0), UF_ADDED_EXCLUDE = (1 << 1) }; // Do various updates when the player sees a cell. Returns whether // exclusion LOS might have been affected. static int player_view_update_at(const coord_def &gc) { const coord_def ep = grid2show(gc); maybe_remove_autoexclusion(gc); int ret = 0; // Set excludes in a radius of 1 around harmful clouds genereated // by neither monsters nor the player. const int cloudidx = env.cgrid(gc); if (cloudidx != EMPTY_CLOUD && !crawl_state.arena) { cloud_struct &cl = env.cloud[cloudidx]; cloud_type ctype = cl.type; bool did_exclude = false; if (!is_harmless_cloud(ctype) && cl.whose == KC_OTHER && cl.killer == KILL_MISC) { for (adjacent_iterator ai(gc, false); ai; ++ai) { // Optionally add exclude, deferring updates. if (!cell_is_solid(*ai)) { bool was_exclusion = is_exclude_root(*ai); set_exclude(*ai, 0, false, false, true); if (!did_exclude && !was_exclusion) ret |= UF_ADDED_EXCLUDE; } } } } // Print tutorial messages for features in LOS. if (Tutorial.tutorial_left) tutorial_observe_cell(gc); if (!player_in_mappable_area()) return (ret); if (is_terrain_changed(gc) || !is_terrain_seen(gc)) ret |= UF_AFFECT_EXCLUDES; set_terrain_seen(gc); set_map_knowledge_obj(gc, to_knowledge(env.show(ep))); set_map_knowledge_detected_mons(gc, false); set_map_knowledge_detected_item(gc, false); #ifdef USE_TILE // We remove any references to mcache when // writing to the background. if (Options.clean_map) env.tile_bk_fg(gc) = get_clean_map_idx(env.tile_fg(ep)); else env.tile_bk_fg(gc) = env.tile_fg(ep); env.tile_bk_bg(gc) = env.tile_bg(ep); #endif if (Options.clean_map && env.show.get_backup(ep)) set_map_knowledge_obj(gc, to_knowledge(env.show.get_backup(ep))); return (ret); } static void player_view_update() { std::vector update_excludes; bool need_update = false; for (radius_iterator ri(&you.get_los()); ri; ++ri) { int flags = player_view_update_at(*ri); if (flags & UF_AFFECT_EXCLUDES) update_excludes.push_back(*ri); if (flags & UF_ADDED_EXCLUDE) need_update = true; } // Update exclusion LOS for possibly affected excludes. update_exclusion_los(update_excludes); // Catch up on deferred updates for cloud excludes. if (need_update) deferred_exclude_update(); } #ifdef USE_TILE void tile_draw_floor() { for (int cy = 0; cy < env.tile_fg.height(); cy++) for (int cx = 0; cx < env.tile_fg.width(); cx++) { const coord_def ep(cx, cy); const coord_def gc = show2grid(ep); int bg = TILE_DNGN_UNSEEN | tile_unseen_flag(gc); if (you.see_cell(gc)) { dungeon_feature_type feat = grid_appearance(gc); bg = tileidx_feature(feat, gc.x, gc.y); if (feat == DNGN_DETECTED_SECRET_DOOR) bg |= TILE_FLAG_WAS_SECRET; else if (is_unknown_stair(gc)) bg |= TILE_FLAG_NEW_STAIR; } // init tiles env.tile_bg[ep.x][ep.y] = bg; env.tile_fg[ep.x][ep.y] = 0; } } #endif static void draw_unseen(screen_buffer_t* buffy, const coord_def &gc) { #ifndef USE_TILE buffy[0] = ' '; buffy[1] = DARKGREY; #else tileidx_unseen(buffy[0], buffy[1], ' ', gc); #endif } static void draw_outside_los(screen_buffer_t* buffy, const coord_def &gc) { #ifndef USE_TILE // Outside the env.show area. buffy[0] = get_map_knowledge_char(gc); buffy[1] = DARKGREY; if (Options.colour_map) buffy[1] = real_colour(get_map_col(gc, false)); #else unsigned int bg = env.tile_bk_bg(gc); unsigned int fg = env.tile_bk_fg(gc); if (bg == 0 && fg == 0) tileidx_unseen(fg, bg, get_map_knowledge_char(gc), gc); buffy[0] = fg; buffy[1] = bg | tile_unseen_flag(gc); #endif } static void draw_player(screen_buffer_t* buffy, const coord_def& gc, const coord_def& ep) { #ifndef USE_TILE // Player overrides everything in cell. buffy[0] = you.symbol; buffy[1] = you.colour; if (you.swimming()) { if (grd(gc) == DNGN_DEEP_WATER) buffy[1] = BLUE; else buffy[1] = CYAN; } if (Options.use_fake_player_cursor) buffy[1] |= COLFLAG_REVERSE; #else buffy[0] = env.tile_fg(ep) = tileidx_player(you.char_class); buffy[1] = env.tile_bg(ep); #endif } static void draw_los(screen_buffer_t* buffy, const coord_def& gc, const coord_def& ep) { #ifndef USE_TILE glyph g = get_show_glyph(env.show(ep)); buffy[0] = g.ch; buffy[1] = g.col; #else buffy[0] = env.tile_fg(ep); buffy[1] = env.tile_bg(ep); #endif } static void draw_los_backup(screen_buffer_t* buffy, const coord_def& gc, const coord_def& ep) { #ifndef USE_TILE buffy[0] = get_map_knowledge_char(gc); buffy[1] = DARKGREY; if (Options.colour_map) buffy[1] = real_colour(get_map_col(gc, false)); #else if (env.tile_bk_fg(gc) != 0 || env.tile_bk_bg(gc) != 0) { buffy[0] = env.tile_bk_fg(gc); buffy[1] = env.tile_bk_bg(gc) | tile_unseen_flag(gc); } else tileidx_unseen(buffy[0], buffy[1], get_map_knowledge_char(gc), gc); #endif } //--------------------------------------------------------------- // // Draws the main window using the character set returned // by get_show_glyph(). // // If monster_updates is set, stealth and conversion checks // take place. Should be set once per turn. // // If show_updates is set, env.show and dependent structures // are updated. Should be set if anything in view has changed. //--------------------------------------------------------------- void viewwindow(bool monster_updates, bool show_updates) { if (you.duration[DUR_TIME_STEP]) return; flush_prev_message(); screen_buffer_t *buffy(crawl_view.vbuf); #ifdef USE_TILE tiles.clear_text_tags(TAG_NAMED_MONSTER); mcache.clear_nonref(); #endif if (show_updates) { you.update_los(); #ifdef USE_TILE tile_draw_floor(); tile_draw_rays(true); tiles.clear_overlays(); #endif env.show.init(); } if (monster_updates && !crawl_state.arena) monster_grid_updates(); if (show_updates) player_view_update(); bool run_dont_draw = you.running && Options.travel_delay < 0 && (!you.running.is_explore() || Options.explore_delay < 0); if (run_dont_draw || you.asleep()) return; cursor_control cs(false); int flash_colour = you.flash_colour; if (flash_colour == BLACK) flash_colour = _viewmap_flash_colour(); const coord_def &tl = crawl_view.viewp; const coord_def br = tl + crawl_view.viewsz - coord_def(1,1); int bufcount = 0; for (rectangle_iterator ri(tl, br); ri; ++ri, bufcount += 2) { // in grid coords const coord_def gc = view2grid(*ri); const coord_def ep = view2show(grid2view(gc)); if (!map_bounds(gc)) draw_unseen(&buffy[bufcount], gc); else if (!crawl_view.in_grid_los(gc)) draw_outside_los(&buffy[bufcount], gc); else if (gc == you.pos() && !crawl_state.arena && !crawl_state.arena_suspended) { draw_player(&buffy[bufcount], gc, ep); } else if (you.see_cell(gc)) draw_los(&buffy[bufcount], gc, ep); else draw_los_backup(&buffy[bufcount], gc, ep); // Alter colour if flashing the characters vision. if (flash_colour) { #ifndef USE_TILE buffy[bufcount + 1] = you.see_cell(gc) ? real_colour(flash_colour) : DARKGREY; #endif } else if (crawl_state.darken_range >= 0) { bool out_of_range = grid_distance(you.pos(), gc) > crawl_state.darken_range || !you.see_cell(gc); #ifndef USE_TILE if (out_of_range) buffy[bufcount + 1] = DARKGREY; #else if (out_of_range && you.see_cell(gc)) buffy[bufcount + 1] |= TILE_FLAG_OOR; #endif } #ifdef USE_TILE // Grey out grids that cannot be reached due to beholders. else if (you.get_beholder(gc)) buffy[bufcount + 1] |= TILE_FLAG_OOR; #endif } // Leaving it this way because short flashes can occur in long ones, // and this simply works without requiring a stack. you.flash_colour = BLACK; #ifndef USE_TILE you.last_view_update = you.num_turns; puttext(crawl_view.viewp.x, crawl_view.viewp.y, crawl_view.viewp.x + crawl_view.viewsz.x - 1, crawl_view.viewp.y + crawl_view.viewsz.y - 1, buffy); update_monster_pane(); #else tiles.set_need_redraw(); tiles.load_dungeon(&buffy[0], crawl_view.vgrdc); tiles.update_inventory(); #endif _debug_pane_bounds(); } //////////////////////////////////////////////////////////////////////////// // Term resize handling (generic). void handle_terminal_resize(bool redraw) { crawl_state.terminal_resized = false; if (crawl_state.terminal_resize_handler) (*crawl_state.terminal_resize_handler)(); else crawl_view.init_geometry(); if (redraw) redraw_screen(); }