Permalink
Browse files

Add a noise meter to the HUD

Noise is one of the more complicated systems in dcss, yet it's barely
represented in the UI.  This is partly because there's just so much
information to represent.  This commit attempts to partially solve this
problem by providing a very simple UI element that summarizes noise
heard by the player during the last turn in the form of a meter.  This
meter peaks at the loudest noise audible at the player's position during
that turn, and will switch between several different colors (details
vary slightly for different interfaces), described below.  I chose to
use sound that has propagated to the player, so more distant events will
be attenuated by distance and dungeon features -- the idea is to give a
measure as to how likely things are to move towards the player's
position.

While experienced players may have internalized a lot of this, most of
this information is not at all apparent to less experienced players, and
I think this UI element is potentially extremely useful (as opposed to
clutter).

1. lightgray: the noise level at the player's position is within a range
that makes it unlikely to propagate outside the player's los (though
this can happen depending on terrain).  Or more simply: this noise is
probably basically ok.  Most low-medium melee damage is in this
category, as are most low-damage spells.

2. yellow: the noise heard at the player's position is likely to
propagate outside the player's los, up to twice the los radius.  Or more
simply: this noise is bad.  Shouting, distant fireballs, high damage
melee attacks, high damage spells (e.g. many bolt spells), door creaks
are in this category.  longbows and bigger crossbows are on the border
of this category.

3. red: the noise is like to be heard at a substantial distance, on an
open level more than 2x the los radius.  Or more simply: this noise is
very bad.  This category includes howler monkeys, lightning bolt, nearby
fireballs, qazlal at pieties above ****.  (Though thunder clouds may
randomly produce red noise at nearly any piety.)

4. lightmagenta: these sounds are off the meter, the loudest sounds you
can make.  Includes: shatter, gong, ood collision, orb apport/pickup

Potential issues/complications:
* This implementation uses a heuristic for branch noise adjustment
that is kind of hacky.  A better (but much more complicated) solution
might be to sample noise at various distances directly from the noise
grid.  Right now I just use a constant addition or subtraction to model
the effects of branch noise; in e.g. shoals or crypt this will push
some of the category boundaries for colors around.
* An alternative approach would be to not use attenuated noise at all,
but find the loudest noise (before propagation) in the player's los.
* In wizmode there is also a numeric version of the meter displayed,
but I decided this is too uninterpretable to be useful to players.
(But, the heuristic for normal noise branches is: in a completely open
area a sound of loudness l will propagate approximately l/.85 spaces.)
* exposing this to the player may reveal some cases where it is not
entirely intuitive that no sound is made. (For example, berserking is
silent (but unstealthy) except when opening doors.) This is good!

The noise bar replaces gold in the HUD; Gozagites now have gold where
the piety bar would be.
  • Loading branch information...
rawlins authored and PleasingFungus committed Feb 14, 2017
1 parent 7b9d119 commit cc4efd4d51a2afb41bb7741c9391f6a292ca4d44
@@ -234,9 +234,15 @@ Place
branch. The starting branch is called Dungeon, so that the place information
will read "Dungeon:1" for a new character.
Gold
Displays the number of gold pieces you have found. Gold is found scattered
around the dungeon, and is primarily used to buy items from shops.
Noise
This is a colored bar indicating the loudness of noise that you heard on your
last turn. The color provides a rough guide to how far away the noise it
indicates might be audible. If the bar is gray, the sound is less likely to
be audible outside of your line of sight (at least in an open area); if it is
yellow, the sound is likely to be audible outside of your line of sight; and
if it is red, the sound will be audible at a substantial distance. If the bar
turns magenta, you have made one of the loudest noises in the dungeon. N.b.:
terrain can reduce or block the spread of noise.
Time
This indicates the amount of time that has passed since entering the dungeon,
@@ -584,7 +584,7 @@ static const duration_def duration_data[] =
{{"", trog_remove_trogs_hand},
{"You feel the effects of Trog's Hand fading.", 1}}, 6},
{ DUR_GOZAG_GOLD_AURA, 0, "", "gold aura", "", "", D_NO_FLAGS,
{{ "", []() { you.props[GOZAG_GOLD_AURA_KEY] = 0; }}}},
{{ "", []() { you.props[GOZAG_GOLD_AURA_KEY] = 0; you.redraw_title = true;}}}},
{ DUR_COLLAPSE, 0, "", "", "collapse", "", D_NO_FLAGS },
{ DUR_BRAINLESS, 0, "", "", "brainless", "", D_NO_FLAGS },
{ DUR_CLUMSY, 0, "", "", "clumsy", "", D_NO_FLAGS },
View
@@ -1527,6 +1527,7 @@ static void _input()
_update_place_info();
crawl_state.clear_god_acting();
}
static bool _can_take_stairs(dungeon_feature_type ftype, bool down,
@@ -2573,6 +2574,13 @@ void world_reacts()
save_game(false);
}
}
// End of a turn.
//
// `los_noise_last_turn` is the value for display -- it needs to persist
// for any calls to print_stats during the next turn. Meanwhile, reset
// the loudest noise tracking for the next world_reacts cycle.
you.los_noise_last_turn = you.los_noise_level;
you.los_noise_level = 0;
}
static command_type _get_next_cmd()
@@ -409,6 +409,7 @@ static void _gold_pile(item_def &corpse, monster_type corpse_class)
const int chance = you.props[GOZAG_GOLD_AURA_KEY].get_int();
if (!x_chance_in_y(chance, chance + 9))
++you.props[GOZAG_GOLD_AURA_KEY].get_int();
you.redraw_title = true;
}
static void _create_monster_hide(const item_def &corpse, bool silent)
@@ -431,6 +431,8 @@ void revive()
you.attribute[ATTR_INVIS_UNCANCELLABLE] = 0;
you.attribute[ATTR_FLIGHT_UNCANCELLABLE] = 0;
you.attribute[ATTR_XP_DRAIN] = 0;
you.los_noise_level = 0;
you.los_noise_last_turn = 0; // silence in death
if (you.duration[DUR_SCRYING])
you.xray_vision = false;
View
@@ -12,6 +12,7 @@
#include <sstream>
#include "ability.h"
#include "areas.h"
#include "branch.h"
#include "colour.h"
#include "describe.h"
@@ -301,6 +302,7 @@ class colour_bar
colour_t m_change_pos;
colour_t m_change_neg;
colour_t m_empty;
int horiz_bar_width;
colour_bar(colour_t default_colour,
colour_t change_pos,
@@ -309,6 +311,7 @@ class colour_bar
bool round = false)
: m_default(default_colour), m_change_pos(change_pos),
m_change_neg(change_neg), m_empty(empty),
horiz_bar_width(-1),
m_old_disp(-1),
m_request_redraw_after(0)
{
@@ -336,7 +339,9 @@ class colour_bar
#if TAG_MAJOR_VERSION == 34
const colour_t temp_colour = temperature_colour(temperature());
#endif
const int width = crawl_view.hudsz.x - (ox - 1);
const int width = (horiz_bar_width != -1) ?
horiz_bar_width :
crawl_view.hudsz.x - (ox - 1);
const int sub_disp = (width * val / max_val);
int disp = width * max(0, val - sub_val) / max_val;
const int old_disp = (m_old_disp < 0) ? sub_disp : m_old_disp;
@@ -477,6 +482,13 @@ static colour_bar MP_Bar(LIGHTBLUE, BLUE, MAGENTA, DARKGREY);
colour_bar Contam_Bar(DARKGREY, DARKGREY, DARKGREY, DARKGREY);
colour_bar Temp_Bar(RED, LIGHTRED, LIGHTBLUE, DARKGREY);
#ifdef USE_TILE_LOCAL
static colour_bar Noise_Bar(WHITE, LIGHTGREY, LIGHTGREY, DARKGREY);
#else
static colour_bar Noise_Bar(LIGHTGREY, LIGHTGREY, MAGENTA, DARKGREY);
#endif
// ----------------------------------------------------------------------
// Status display
// ----------------------------------------------------------------------
@@ -582,6 +594,103 @@ static void _print_stats_temperature(int x, int y)
}
#endif
/*
* Print the noise bar to the HUD with appropriate coloring.
* if in wizmode, also print the numeric noise value.
*/
static void _print_stats_noise(int x, int y)
{
bool silence = silenced(you.pos());
int level = silence ? 0 : you.get_noise_perception(true);
CGOTOXY(x, y, GOTO_STAT);
textcolour(HUD_CAPTION_COLOUR);
cprintf("Noise: ");
colour_t noisecolour;
// This is calibrated roughly so that in an open-ish area:
// LIGHTGREY = not very likely to carry outside of your los
// (though it is possible depending on terrain).
// YELLOW = likely to carry outside of your los, up to double.
// RED = likely to carry at least 16 spaces, up to much further.
// LIGHTMAGENTA = really f*cking loud. (Gong, etc.)
// In more enclosed areas, these values will be attenuated,
// and this isn't represented.
// See player::get_noise_perception for the mapping from internal noise
// values to this 0-1000 scale.
// NOTE: This color scheme is duplicated in player.js.
if (level <= 333)
noisecolour = LIGHTGREY;
else if (level <= 666)
noisecolour = YELLOW;
else if (level < 1000)
{
noisecolour = RED;
} else {
noisecolour = LIGHTMAGENTA;
}
int bar_position;
if (you.wizard)
{
Noise_Bar.horiz_bar_width = 6;
bar_position = 10;
// numeric noise level, basically the internal value used by noise
// propagation (see shout.cc:noisy). The exact value is too hard to
// interpret to show outside of wizmode, because noise propagation is
// very complicated.
CGOTOXY(x + bar_position - 3, y, GOTO_STAT);
textcolour(noisecolour);
CPRINTF("%2d", you.get_noise_perception(false));
} else {
Noise_Bar.horiz_bar_width = 9;
bar_position = 7;
}
if (silence)
{
CGOTOXY(x + bar_position, y, GOTO_STAT);
textcolour(LIGHTMAGENTA);
if (you.wizard)
{
CPRINTF("(Sil) ");
} else {
CPRINTF("(Sil) "); // These need to be one extra wide in case silence happens
// immediately after super-loud (magenta) noise
}
} else {
if (level == 1000)
{
// the bar goes up to 11 for extra loud sounds! (Well, it's really 10.)
Noise_Bar.horiz_bar_width += 1;
} else {
CGOTOXY(x + 16, y, GOTO_STAT);
CPRINTF(" "); // clean up after the extra wide bar
}
#ifndef USE_TILE_LOCAL
// use the previous color for negative change in console; there's a
// visual difference in bar width. Negative change doesn't get shown
// in local tiles.
Noise_Bar.m_change_neg = Noise_Bar.m_default;
#endif
Noise_Bar.m_default = noisecolour;
Noise_Bar.m_change_pos = noisecolour;
Noise_Bar.draw(x + bar_position, y, div_round_up((level * Noise_Bar.horiz_bar_width), 1000), Noise_Bar.horiz_bar_width);
}
}
static void _print_stats_gold(int x, int y, colour_t colour)
{
CGOTOXY(x, y, GOTO_STAT);
textcolour(HUD_CAPTION_COLOUR);
CPRINTF("Gold:");
CGOTOXY(x+6, y, GOTO_STAT);
if (you.duration[DUR_GOZAG_GOLD_AURA])
textcolour(LIGHTBLUE);
else
textcolour(colour);
CPRINTF("%-6d", you.gold);
}
static void _print_stats_mp(int x, int y)
{
#if TAG_MAJOR_VERSION == 34
@@ -1186,18 +1295,21 @@ static void _redraw_title()
string piety = _god_asterisks();
textcolour(_god_status_colour(YELLOW));
if ((unsigned int)(strwidth(species) + strwidth(god) + strwidth(piety) + 1)
<= WIDTH)
{
const unsigned int textwidth = (unsigned int)(strwidth(species) + strwidth(god) + strwidth(piety) + 1);
if (textwidth <= WIDTH)
NOWRAP_EOL_CPRINTF(" %s", piety.c_str());
}
else if ((unsigned int)(strwidth(species) + strwidth(god) + strwidth(piety) + 1)
== (WIDTH + 1))
else if (textwidth == (WIDTH + 1))
{
//mottled draconian of TSO doesn't fit by one symbol,
//so we remove leading space.
NOWRAP_EOL_CPRINTF("%s", piety.c_str());
}
clear_to_end_of_line();
if (you_worship(GOD_GOZAG))
{
// "Mottled Draconian of Gozag Gold: 99999" just fits
_print_stats_gold(textwidth + 2, 2, _god_status_colour(god_colour(you.religion)));
}
}
else if (you.char_class == JOB_MONK && you.species != SP_DEMIGOD
&& !had_gods())
@@ -1206,9 +1318,9 @@ static void _redraw_title()
textcolour(DARKGREY);
if ((unsigned int)(strwidth(species) + strwidth(godpiety) + 1) <= WIDTH)
NOWRAP_EOL_CPRINTF(" %s", godpiety.c_str());
clear_to_end_of_line();
}
clear_to_end_of_line();
textcolour(LIGHTGREY);
}
@@ -1329,19 +1441,13 @@ void print_stats()
int yhack = 0;
#endif
// Line 9 is Gold and Turns
// Line 9 is Noise and Turns
#ifdef USE_TILE_LOCAL
if (!tiles.is_using_small_layout())
#endif
{
// Increase y-value for all following lines.
yhack++;
CGOTOXY(1+6, 8 + yhack, GOTO_STAT);
if (you.duration[DUR_GOZAG_GOLD_AURA])
textcolour(LIGHTBLUE);
else
textcolour(HUD_VALUE_COLOUR);
CPRINTF("%-6d", you.gold);
_print_stats_noise(1, 8+yhack);
}
if (you.wield_change)
@@ -1467,7 +1573,6 @@ void draw_border()
#else
int yhack = 0;
#endif
CGOTOXY(1, 9 + yhack, GOTO_STAT); CPRINTF("Gold:");
CGOTOXY(19, 9 + yhack, GOTO_STAT);
CPRINTF(Options.show_game_time ? "Time:" : "Turn:");
// Line 8 is exp pool, Level
View
@@ -5360,6 +5360,11 @@ player::player()
game_seeds[i] = get_uint32();
old_hunger = hunger;
los_noise_level = 0; ///< temporary slot for loud noise levels
los_noise_last_turn = 0;
///< loudest noise heard on the last turn, for HUD display
transit_stair = DNGN_UNSEEN;
entering_level = false;
@@ -5638,6 +5643,48 @@ int calc_hunger(int food_cost)
return food_cost;
}
/*
* Approximate the loudest noise the player heard in the last
* turn, possibly rescaling. This gets updated every
* `world_reacts`. If `adjusted` is set to true, this rescales
* noise on a 0-1000 scale according to some breakpoints that
* I have hand-calibrated. Otherwise, it returns the raw noise
* value (approximately from 0 to 40). The breakpoints aim to
* approximate 1x los radius, 2x los radius, and 3x los radius
* relative to an open area.
*
* @param adjusted Whether to rescale the noise level.
*
* @return The (scaled or unscaled) noise level heard by the player.
*/
int player::get_noise_perception(bool adjusted) const
{
// los_noise_last_turn is already normalized for the branch's ambient
// noise.
const int level = los_noise_last_turn;
static const int BAR_MAX = 1000; // TODO: export to output.cc & webtiles
if (!adjusted)
return div_rand_round(level, BAR_MAX);
static const vector<int> NOISE_BREAKPOINTS = { 0, 6000, 7000, 16000 };
const int BAR_FRAC = BAR_MAX / (NOISE_BREAKPOINTS.size() - 1);
for (size_t i = 1; i < NOISE_BREAKPOINTS.size(); ++i)
{
const int breakpoint = NOISE_BREAKPOINTS[i];
if (level > breakpoint)
continue;
const int prev_break = NOISE_BREAKPOINTS[i-1];
// what fragment of this breakpoint does the noise fill up?
const int within_segment = (level - prev_break) * BAR_FRAC / breakpoint;
// that fragment + previous breakpoints passed is our total noise.
return within_segment + (i - 1) * BAR_FRAC;
// example: 10k noise. that's 4k past the 6k breakpoint and into the 7k
// (4k * 333 / 7k) + 333, or a bit more than half the bar.
}
return BAR_MAX;
}
bool player::paralysed() const
{
return duration[DUR_PARALYSIS];
@@ -7355,7 +7402,10 @@ void player::awaken()
void player::check_awaken(int disturbance)
{
if (asleep() && x_chance_in_y(disturbance + 1, 50))
{
awaken();
dprf("Disturbance of intensity %d awoke player", disturbance);
}
}
int player::beam_resists(bolt &beam, int hurted, bool doEffects, string source)
@@ -7499,6 +7549,7 @@ void player::set_gold(int amount)
else if (old_gold >= cost && gold < cost)
power.display(false, "You no longer have enough gold to %s.");
}
you.redraw_title = true;
}
}
}
@@ -409,6 +409,10 @@ class player : public actor
int old_hunger; // used for hunger delta-meter (see output.cc)
// the loudest noise level the player has experienced in los this turn
int los_noise_level;
int los_noise_last_turn;
// Set when the character is going to a new level, to guard against levgen
// failures
dungeon_feature_type transit_stair;
@@ -729,6 +733,7 @@ class player : public actor
bool cancellable_flight() const;
bool permanent_flight() const;
bool racial_permanent_flight() const;
int get_noise_perception(bool adjusted = true) const;
bool paralysed() const override;
bool cannot_move() const override;
Oops, something went wrong.

0 comments on commit cc4efd4

Please sign in to comment.