From 73a29b339c40c4f6c43ecf7c40e140490acefcf7 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Sun, 24 May 2020 02:37:10 -0400
Subject: [PATCH 1/7] Added chat bubbles. Refactored bubble+name+hp+energy into
 new overhead widget

---
 .../voxygen/element/frames/bubble/bottom.png  |   3 +
 .../element/frames/bubble/bottom_left.png     |   3 +
 .../element/frames/bubble/bottom_right.png    |   3 +
 assets/voxygen/element/frames/bubble/left.png |   3 +
 assets/voxygen/element/frames/bubble/mid.png  |   3 +
 .../voxygen/element/frames/bubble/right.png   |   3 +
 assets/voxygen/element/frames/bubble/tail.png |   3 +
 assets/voxygen/element/frames/bubble/top.png  |   3 +
 .../element/frames/bubble/top_left.png        |   3 +
 .../element/frames/bubble/top_right.png       |   3 +
 voxygen/src/hud/img_ids.rs                    |  12 +
 voxygen/src/hud/mod.rs                        | 518 ++++++------------
 voxygen/src/hud/overhead.rs                   | 300 ++++++++++
 13 files changed, 509 insertions(+), 351 deletions(-)
 create mode 100644 assets/voxygen/element/frames/bubble/bottom.png
 create mode 100644 assets/voxygen/element/frames/bubble/bottom_left.png
 create mode 100644 assets/voxygen/element/frames/bubble/bottom_right.png
 create mode 100644 assets/voxygen/element/frames/bubble/left.png
 create mode 100644 assets/voxygen/element/frames/bubble/mid.png
 create mode 100644 assets/voxygen/element/frames/bubble/right.png
 create mode 100644 assets/voxygen/element/frames/bubble/tail.png
 create mode 100644 assets/voxygen/element/frames/bubble/top.png
 create mode 100644 assets/voxygen/element/frames/bubble/top_left.png
 create mode 100644 assets/voxygen/element/frames/bubble/top_right.png
 create mode 100644 voxygen/src/hud/overhead.rs

diff --git a/assets/voxygen/element/frames/bubble/bottom.png b/assets/voxygen/element/frames/bubble/bottom.png
new file mode 100644
index 0000000000..a133651d2a
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/bottom.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b2389dc62c0765c9ed56e53afab639c7fcb90656d83a051e8cf513cda926804
+size 136
diff --git a/assets/voxygen/element/frames/bubble/bottom_left.png b/assets/voxygen/element/frames/bubble/bottom_left.png
new file mode 100644
index 0000000000..514e7f025d
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/bottom_left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2983324f979fe0ee212e7b4527d4d2ae042ec7da418cee01cdd0637d9410b042
+size 3485
diff --git a/assets/voxygen/element/frames/bubble/bottom_right.png b/assets/voxygen/element/frames/bubble/bottom_right.png
new file mode 100644
index 0000000000..418b586b89
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/bottom_right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7681e4b43db2d68f8576b26e91c8395f292b9aab76b0fac27ef2f5239e438766
+size 3639
diff --git a/assets/voxygen/element/frames/bubble/left.png b/assets/voxygen/element/frames/bubble/left.png
new file mode 100644
index 0000000000..533725330d
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6da4e97492532b7c8b83119bd419c5689c00149e217c26dcff6e9628e0eef6de
+size 124
diff --git a/assets/voxygen/element/frames/bubble/mid.png b/assets/voxygen/element/frames/bubble/mid.png
new file mode 100644
index 0000000000..f00c3ea319
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/mid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4adcd985ae2c5fb0f3f9c32995c1da886f91503ca238052e4eec87fd51831efe
+size 109
diff --git a/assets/voxygen/element/frames/bubble/right.png b/assets/voxygen/element/frames/bubble/right.png
new file mode 100644
index 0000000000..210af16f17
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b17fa4cacc5617de542e8ea054113dba1e8aa19272567f4a8cf026d1e849d0d0
+size 125
diff --git a/assets/voxygen/element/frames/bubble/tail.png b/assets/voxygen/element/frames/bubble/tail.png
new file mode 100644
index 0000000000..19889b332f
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/tail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:16f8d6165a5071be33830a33215bbcd5fc4a7783a932bff396df98167e5241bb
+size 227
diff --git a/assets/voxygen/element/frames/bubble/top.png b/assets/voxygen/element/frames/bubble/top.png
new file mode 100644
index 0000000000..0c88a60f54
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/top.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:14518edba8bc71829422acd9c3e0c76dc9c78678ed1acc87e81257667eb46a79
+size 136
diff --git a/assets/voxygen/element/frames/bubble/top_left.png b/assets/voxygen/element/frames/bubble/top_left.png
new file mode 100644
index 0000000000..e1b770cec8
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/top_left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4fd9c69f48c863371cef8ed8ea332710b975583b6ba7f0fa91359042a471d32
+size 184
diff --git a/assets/voxygen/element/frames/bubble/top_right.png b/assets/voxygen/element/frames/bubble/top_right.png
new file mode 100644
index 0000000000..d994a4198c
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble/top_right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8127cb08af479b2e08f3a4decfd5640c97095d7333bf7cd40a4a3e61c8cf09ae
+size 219
diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs
index 11f8fcc76b..bec188275e 100644
--- a/voxygen/src/hud/img_ids.rs
+++ b/voxygen/src/hud/img_ids.rs
@@ -272,6 +272,18 @@ image_ids! {
         progress_frame: "voxygen.element.frames.progress_bar",
         progress: "voxygen.element.misc_bg.progress",
 
+        // Chat bubbles
+        chat_bubble_top_left: "voxygen.element.frames.bubble.top_left",
+        chat_bubble_top: "voxygen.element.frames.bubble.top",
+        chat_bubble_top_right: "voxygen.element.frames.bubble.top_right",
+        chat_bubble_left: "voxygen.element.frames.bubble.left",
+        chat_bubble_mid: "voxygen.element.frames.bubble.mid",
+        chat_bubble_right: "voxygen.element.frames.bubble.right",
+        chat_bubble_bottom_left: "voxygen.element.frames.bubble.bottom_left",
+        chat_bubble_bottom: "voxygen.element.frames.bubble.bottom",
+        chat_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right",
+        chat_bubble_tail: "voxygen.element.frames.bubble.tail",
+
         <BlankGraphic>
         nothing: (),
     }
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index c5a1e2878b..f4743ec34f 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -7,6 +7,7 @@ mod img_ids;
 mod item_imgs;
 mod map;
 mod minimap;
+mod overhead;
 mod popup;
 mod settings_window;
 mod skillbar;
@@ -102,17 +103,6 @@ widget_ids! {
         crosshair_inner,
         crosshair_outer,
 
-        // Character Names
-        name_tags[],
-        name_tags_bgs[],
-        levels[],
-        levels_skull[],
-        // Health Bars
-        health_bars[],
-        mana_bars[],
-        health_bar_fronts[],
-        health_bar_backs[],
-
         // SCT
         player_scts[],
         player_sct_bgs[],
@@ -125,6 +115,8 @@ widget_ids! {
         sct_bgs[],
         scts[],
 
+        overheads[],
+
         // Intro Text
         intro_bg,
         intro_text,
@@ -643,13 +635,9 @@ impl Hud {
                 }
             }
 
-            // Nametags and healthbars
-
             // Max amount the sct font size increases when "flashing"
             const FLASH_MAX: f32 = 25.0;
-            const BARSIZE: f64 = 2.0;
-            const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
-            const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
+
             // Get player position.
             let player_pos = client
                 .state()
@@ -657,265 +645,6 @@ impl Hud {
                 .read_storage::<comp::Pos>()
                 .get(client.entity())
                 .map_or(Vec3::zero(), |pos| pos.0);
-            let mut name_id_walker = self.ids.name_tags.walk();
-            let mut name_id_bg_walker = self.ids.name_tags_bgs.walk();
-            let mut level_id_walker = self.ids.levels.walk();
-            let mut level_skull_id_walker = self.ids.levels_skull.walk();
-            let mut health_id_walker = self.ids.health_bars.walk();
-            let mut mana_id_walker = self.ids.mana_bars.walk();
-            let mut health_back_id_walker = self.ids.health_bar_backs.walk();
-            let mut health_front_id_walker = self.ids.health_bar_fronts.walk();
-            let mut sct_bg_id_walker = self.ids.sct_bgs.walk();
-            let mut sct_id_walker = self.ids.scts.walk();
-
-            // Render Health Bars
-            for (pos, stats, energy, height_offset, hp_floater_list) in (
-                &entities,
-                &pos,
-                interpolated.maybe(),
-                &stats,
-                &energy,
-                scales.maybe(),
-                &bodies,
-                &hp_floater_lists,
-            )
-                .join()
-                .filter(|(entity, _, _, stats, _, _, _, _)| {
-                    *entity != me && !stats.is_dead
-                    //&& stats.health.current() != stats.health.maximum()
-                })
-                // Don't show outside a certain range
-                .filter(|(_, pos, _, _, _, _, _, hpfl)| {
-                    pos.0.distance_squared(player_pos)
-                        < (if hpfl
-                            .time_since_last_dmg_by_me
-                            .map_or(false, |t| t < NAMETAG_DMG_TIME)
-                        {
-                            NAMETAG_DMG_RANGE
-                        } else {
-                            NAMETAG_RANGE
-                        })
-                        .powi(2)
-                })
-                .map(|(_, pos, interpolated, stats, energy, scale, body, f)| {
-                    (
-                        interpolated.map_or(pos.0, |i| i.pos),
-                        stats,
-                        energy,
-                        // TODO: when body.height() is more accurate remove the 2.0
-                        body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
-                        f,
-                    )
-                })
-            {
-                let back_id = health_back_id_walker.next(
-                    &mut self.ids.health_bar_backs,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-                let health_bar_id = health_id_walker.next(
-                    &mut self.ids.health_bars,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-                let mana_bar_id = mana_id_walker.next(
-                    &mut self.ids.mana_bars,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-                let front_id = health_front_id_walker.next(
-                    &mut self.ids.health_bar_fronts,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-                let hp_percentage =
-                    stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0;
-                let energy_percentage = energy.current() as f64 / energy.maximum() as f64 * 100.0;
-                let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
-                let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
-
-                let ingame_pos = pos + Vec3::unit_z() * height_offset;
-
-                // Background
-                Image::new(self.imgs.enemy_health_bg)
-                    .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
-                    .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
-                    .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
-                    .position_ingame(ingame_pos)
-                    .set(back_id, ui_widgets);
-
-                // % HP Filling
-                Image::new(self.imgs.enemy_bar)
-                    .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
-                    .x_y(
-                        (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
-                        MANA_BAR_Y + 7.5,
-                    )
-                    .color(Some(if hp_percentage <= 25.0 {
-                        crit_hp_color
-                    } else if hp_percentage <= 50.0 {
-                        LOW_HP_COLOR
-                    } else {
-                        HP_COLOR
-                    }))
-                    .position_ingame(ingame_pos)
-                    .set(health_bar_id, ui_widgets);
-                // % Mana Filling
-                Rectangle::fill_with(
-                    [
-                        72.0 * (energy.current() as f64 / energy.maximum() as f64) * BARSIZE,
-                        MANA_BAR_HEIGHT,
-                    ],
-                    MANA_COLOR,
-                )
-                .x_y(
-                    ((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE,
-                    MANA_BAR_Y, //-32.0,
-                )
-                .position_ingame(ingame_pos)
-                .set(mana_bar_id, ui_widgets);
-
-                // Foreground
-                Image::new(self.imgs.enemy_health)
-                    .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
-                    .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
-                    .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
-                    .position_ingame(ingame_pos)
-                    .set(front_id, ui_widgets);
-
-                // Enemy SCT
-                if let Some(floaters) = Some(hp_floater_list)
-                    .filter(|fl| !fl.floaters.is_empty() && global_state.settings.gameplay.sct)
-                    .map(|l| &l.floaters)
-                {
-                    // Colors
-                    const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
-                    const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
-                    const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
-                    const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
-                    const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
-                    const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
-                    const DAMAGE_COLORS: [Rgb<f32>; 6] = [
-                        WHITE,
-                        LIGHT_OR,
-                        LIGHT_MED_OR,
-                        MED_OR,
-                        DARK_ORANGE,
-                        RED_ORANGE,
-                    ];
-                    // Largest value that select the first color is 40, then it shifts colors
-                    // every 5
-                    let font_col = |font_size: u32| {
-                        DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
-                    };
-
-                    if global_state.settings.gameplay.sct_damage_batch {
-                        let number_speed = 50.0; // Damage number speed
-                        let sct_bg_id = sct_bg_id_walker
-                            .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
-                        let sct_id = sct_id_walker
-                            .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
-                        // Calculate total change
-                        // Ignores healing
-                        let hp_damage = floaters.iter().fold(0, |acc, f| {
-                            if f.hp_change < 0 {
-                                acc + f.hp_change
-                            } else {
-                                acc
-                            }
-                        });
-                        let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
-                        let timer = floaters
-                            .last()
-                            .expect("There must be at least one floater")
-                            .timer;
-                        // Increase font size based on fraction of maximum health
-                        // "flashes" by having a larger size in the first 100ms
-                        let font_size = 30
-                            + (max_hp_frac * 30.0) as u32
-                            + if timer < 0.1 {
-                                (FLASH_MAX * (1.0 - timer / 0.1)) as u32
-                            } else {
-                                0
-                            };
-                        let font_col = font_col(font_size);
-                        // Timer sets the widget offset
-                        let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
-                            * number_speed)
-                            + 100.0;
-                        // Timer sets text transparency
-                        let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
-
-                        Text::new(&format!("{}", (hp_damage).abs()))
-                            .font_size(font_size)
-                            .font_id(self.fonts.cyri.conrod_id)
-                            .color(Color::Rgba(0.0, 0.0, 0.0, fade))
-                            .x_y(0.0, y - 3.0)
-                            .position_ingame(ingame_pos)
-                            .set(sct_bg_id, ui_widgets);
-                        Text::new(&format!("{}", hp_damage.abs()))
-                            .font_size(font_size)
-                            .font_id(self.fonts.cyri.conrod_id)
-                            .x_y(0.0, y)
-                            .color(if hp_damage < 0 {
-                                Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
-                            } else {
-                                Color::Rgba(0.1, 1.0, 0.1, fade)
-                            })
-                            .position_ingame(ingame_pos)
-                            .set(sct_id, ui_widgets);
-                    } else {
-                        for floater in floaters {
-                            let number_speed = 250.0; // Single Numbers Speed
-                            let sct_bg_id = sct_bg_id_walker
-                                .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
-                            let sct_id = sct_id_walker
-                                .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
-                            // Calculate total change
-                            let max_hp_frac =
-                                floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
-                            // Increase font size based on fraction of maximum health
-                            // "flashes" by having a larger size in the first 100ms
-                            let font_size = 30
-                                + (max_hp_frac * 30.0) as u32
-                                + if floater.timer < 0.1 {
-                                    (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
-                                } else {
-                                    0
-                                };
-                            let font_col = font_col(font_size);
-                            // Timer sets the widget offset
-                            let y = (floater.timer as f64
-                                / crate::ecs::sys::floater::HP_SHOWTIME as f64
-                                * number_speed)
-                                + 100.0;
-                            // Timer sets text transparency
-                            let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
-                                * 0.25)
-                                + 0.2;
-
-                            Text::new(&format!("{}", (floater.hp_change).abs()))
-                                .font_size(font_size)
-                                .font_id(self.fonts.cyri.conrod_id)
-                                .color(if floater.hp_change < 0 {
-                                    Color::Rgba(0.0, 0.0, 0.0, fade)
-                                } else {
-                                    Color::Rgba(0.0, 0.0, 0.0, 1.0)
-                                })
-                                .x_y(0.0, y - 3.0)
-                                .position_ingame(ingame_pos)
-                                .set(sct_bg_id, ui_widgets);
-                            Text::new(&format!("{}", (floater.hp_change).abs()))
-                                .font_size(font_size)
-                                .font_id(self.fonts.cyri.conrod_id)
-                                .x_y(0.0, y)
-                                .color(if floater.hp_change < 0 {
-                                    Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
-                                } else {
-                                    Color::Rgba(0.1, 1.0, 0.1, 1.0)
-                                })
-                                .position_ingame(ingame_pos)
-                                .set(sct_id, ui_widgets);
-                        }
-                    }
-                }
-            }
 
             if global_state.settings.gameplay.sct {
                 // Render Player SCT numbers
@@ -1156,21 +885,26 @@ impl Hud {
                 }
             }
 
-            // Render Name Tags
-            for (pos, name, level, height_offset) in (
+            let mut overhead_walker = self.ids.overheads.walk();
+            let mut sct_walker = self.ids.scts.walk();
+            let mut sct_bg_walker = self.ids.sct_bgs.walk();
+
+            // Render overhead name tags and health bars
+            for (pos, name, stats, energy, height_offset, hpfl) in (
                 &entities,
                 &pos,
                 interpolated.maybe(),
                 &stats,
+                &energy,
                 players.maybe(),
                 scales.maybe(),
                 &bodies,
                 &hp_floater_lists,
             )
                 .join()
-                .filter(|(entity, _, _, stats, _, _, _, _)| *entity != me && !stats.is_dead)
+                .filter(|(entity, _, _, stats, _, _, _, _, _)| *entity != me && !stats.is_dead)
                 // Don't show outside a certain range
-                .filter(|(_, pos, _, _, _, _, _, hpfl)| {
+                .filter(|(_, pos, _, _, _, _, _, _, hpfl)| {
                     pos.0.distance_squared(player_pos)
                         < (if hpfl
                             .time_since_last_dmg_by_me
@@ -1182,7 +916,7 @@ impl Hud {
                         })
                         .powi(2)
                 })
-                .map(|(_, pos, interpolated, stats, player, scale, body, _)| {
+                .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl)| {
                     // TODO: This is temporary
                     // If the player used the default character name display their name instead
                     let name = if stats.name == "Character Name" {
@@ -1192,87 +926,169 @@ impl Hud {
                     };
                     (
                         interpolated.map_or(pos.0, |i| i.pos),
-                        format!("{}", name),
-                        stats.level,
+                        name,
+                        stats,
+                        energy,
+                        // TODO: when body.height() is more accurate remove the 2.0
                         body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
+                        hpfl,
                     )
                 })
             {
-                let name_id = name_id_walker.next(
-                    &mut self.ids.name_tags,
+                let overhead_id = overhead_walker.next(
+                    &mut self.ids.overheads,
                     &mut ui_widgets.widget_id_generator(),
                 );
-                let name_bg_id = name_id_bg_walker.next(
-                    &mut self.ids.name_tags_bgs,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-                let level_id = level_id_walker
-                    .next(&mut self.ids.levels, &mut ui_widgets.widget_id_generator());
-                let level_skull_id = level_skull_id_walker.next(
-                    &mut self.ids.levels_skull,
-                    &mut ui_widgets.widget_id_generator(),
-                );
-
                 let ingame_pos = pos + Vec3::unit_z() * height_offset;
 
-                // Name
-                Text::new(&name)
-                    .font_id(self.fonts.cyri.conrod_id)
-                    .font_size(30)
-                    .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
-                    .x_y(-1.0, MANA_BAR_Y + 48.0)
-                    .position_ingame(ingame_pos)
-                    .set(name_bg_id, ui_widgets);
-                Text::new(&name)
-                    .font_id(self.fonts.cyri.conrod_id)
-                    .font_size(30)
-                    .color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
-                    .x_y(0.0, MANA_BAR_Y + 50.0)
-                    .position_ingame(ingame_pos)
-                    .set(name_id, ui_widgets);
+                // Chat bubble, name, level, and hp bars
+                overhead::Overhead::new(
+                    &name,
+                    stats,
+                    energy,
+                    own_level,
+                    self.pulse,
+                    &self.imgs,
+                    &self.fonts,
+                )
+                .x_y(0.0, 100.0)
+                .position_ingame(ingame_pos)
+                .set(overhead_id, ui_widgets);
 
-                // Level
-                const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
-                const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
-                const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
-                let op_level = level.level();
-                let level_str = format!("{}", op_level);
-                // Change visuals of the level display depending on the player level/opponent
-                // level
-                let level_comp = op_level as i64 - own_level as i64;
-                // + 10 level above player -> skull
-                // + 5-10 levels above player -> high
-                // -5 - +5 levels around player level -> equal
-                // - 5 levels below player -> low
-                Text::new(if level_comp < 10 { &level_str } else { "?" })
-                    .font_id(self.fonts.cyri.conrod_id)
-                    .font_size(if op_level > 9 && level_comp < 10 {
-                        14
+                // Enemy SCT
+                if global_state.settings.gameplay.sct && !hpfl.floaters.is_empty() {
+                    let floaters = &hpfl.floaters;
+
+                    // Colors
+                    const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
+                    const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
+                    const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
+                    const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
+                    const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
+                    const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
+                    const DAMAGE_COLORS: [Rgb<f32>; 6] = [
+                        WHITE,
+                        LIGHT_OR,
+                        LIGHT_MED_OR,
+                        MED_OR,
+                        DARK_ORANGE,
+                        RED_ORANGE,
+                    ];
+                    // Largest value that select the first color is 40, then it shifts colors
+                    // every 5
+                    let font_col = |font_size: u32| {
+                        DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
+                    };
+
+                    if global_state.settings.gameplay.sct_damage_batch {
+                        let number_speed = 50.0; // Damage number speed
+                        let sct_id = sct_walker
+                            .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
+                        let sct_bg_id = sct_bg_walker
+                            .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
+                        // Calculate total change
+                        // Ignores healing
+                        let hp_damage = floaters.iter().fold(0, |acc, f| {
+                            if f.hp_change < 0 {
+                                acc + f.hp_change
+                            } else {
+                                acc
+                            }
+                        });
+                        let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32;
+                        let timer = floaters
+                            .last()
+                            .expect("There must be at least one floater")
+                            .timer;
+                        // Increase font size based on fraction of maximum health
+                        // "flashes" by having a larger size in the first 100ms
+                        let font_size = 30
+                            + (max_hp_frac * 30.0) as u32
+                            + if timer < 0.1 {
+                                (FLASH_MAX * (1.0 - timer / 0.1)) as u32
+                            } else {
+                                0
+                            };
+                        let font_col = font_col(font_size);
+                        // Timer sets the widget offset
+                        let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
+                            * number_speed)
+                            + 100.0;
+                        // Timer sets text transparency
+                        let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2;
+
+                        Text::new(&format!("{}", (hp_damage).abs()))
+                            .font_size(font_size)
+                            .font_id(self.fonts.cyri.conrod_id)
+                            .color(Color::Rgba(0.0, 0.0, 0.0, fade))
+                            .x_y(0.0, y - 3.0)
+                            .position_ingame(ingame_pos)
+                            .set(sct_bg_id, ui_widgets);
+                        Text::new(&format!("{}", hp_damage.abs()))
+                            .font_size(font_size)
+                            .font_id(self.fonts.cyri.conrod_id)
+                            .x_y(0.0, y)
+                            .color(if hp_damage < 0 {
+                                Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
+                            } else {
+                                Color::Rgba(0.1, 1.0, 0.1, fade)
+                            })
+                            .position_ingame(ingame_pos)
+                            .set(sct_id, ui_widgets);
                     } else {
-                        15
-                    })
-                    .color(if level_comp > 4 {
-                        HIGH
-                    } else if level_comp < -5 {
-                        LOW
-                    } else {
-                        EQUAL
-                    })
-                    .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
-                    .position_ingame(ingame_pos)
-                    .set(level_id, ui_widgets);
-                if level_comp > 9 {
-                    let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
-                    Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
-                        self.imgs.skull_2
-                    } else {
-                        self.imgs.skull
-                    })
-                    .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
-                    .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
-                    .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
-                    .position_ingame(ingame_pos)
-                    .set(level_skull_id, ui_widgets);
+                        for floater in floaters {
+                            let number_speed = 250.0; // Single Numbers Speed
+                            let sct_id = sct_walker
+                                .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
+                            let sct_bg_id = sct_bg_walker
+                                .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
+                            // Calculate total change
+                            let max_hp_frac =
+                                floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
+                            // Increase font size based on fraction of maximum health
+                            // "flashes" by having a larger size in the first 100ms
+                            let font_size = 30
+                                + (max_hp_frac * 30.0) as u32
+                                + if floater.timer < 0.1 {
+                                    (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32
+                                } else {
+                                    0
+                                };
+                            let font_col = font_col(font_size);
+                            // Timer sets the widget offset
+                            let y = (floater.timer as f64
+                                / crate::ecs::sys::floater::HP_SHOWTIME as f64
+                                * number_speed)
+                                + 100.0;
+                            // Timer sets text transparency
+                            let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer)
+                                * 0.25)
+                                + 0.2;
+
+                            Text::new(&format!("{}", (floater.hp_change).abs()))
+                                .font_size(font_size)
+                                .font_id(self.fonts.cyri.conrod_id)
+                                .color(if floater.hp_change < 0 {
+                                    Color::Rgba(0.0, 0.0, 0.0, fade)
+                                } else {
+                                    Color::Rgba(0.0, 0.0, 0.0, 1.0)
+                                })
+                                .x_y(0.0, y - 3.0)
+                                .position_ingame(ingame_pos)
+                                .set(sct_bg_id, ui_widgets);
+                            Text::new(&format!("{}", (floater.hp_change).abs()))
+                                .font_size(font_size)
+                                .font_id(self.fonts.cyri.conrod_id)
+                                .x_y(0.0, y)
+                                .color(if floater.hp_change < 0 {
+                                    Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
+                                } else {
+                                    Color::Rgba(0.1, 1.0, 0.1, 1.0)
+                                })
+                                .position_ingame(ingame_pos)
+                                .set(sct_id, ui_widgets);
+                        }
+                    }
                 }
             }
         }
diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs
new file mode 100644
index 0000000000..2f66aa5493
--- /dev/null
+++ b/voxygen/src/hud/overhead.rs
@@ -0,0 +1,300 @@
+use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
+use crate::ui::{fonts::ConrodVoxygenFonts, Ingameable};
+use common::comp::{Energy, Stats};
+use conrod_core::{
+    position::Align,
+    widget::{self, Image, Rectangle, Text},
+    widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
+};
+
+widget_ids! {
+    struct Ids {
+        // Chat bubble
+        chat_bubble_text,
+        chat_bubble_text2,
+        chat_bubble_top_left,
+        chat_bubble_top,
+        chat_bubble_top_right,
+        chat_bubble_left,
+        chat_bubble_mid,
+        chat_bubble_right,
+        chat_bubble_bottom_left,
+        chat_bubble_bottom,
+        chat_bubble_bottom_right,
+        chat_bubble_tail,
+
+        // Name
+        name_bg,
+        name,
+
+        // HP
+        level,
+        level_skull,
+        health_bar,
+        health_bar_bg,
+        mana_bar,
+        health_bar_fg,
+    }
+}
+
+/// ui widget containing everything that goes over a character's head
+/// (Speech bubble, Name, Level, HP/energy bars, etc.)
+#[derive(WidgetCommon)]
+pub struct Overhead<'a> {
+    name: &'a str,
+    stats: &'a Stats,
+    energy: &'a Energy,
+    own_level: u32,
+    pulse: f32,
+    imgs: &'a Imgs,
+    fonts: &'a ConrodVoxygenFonts,
+    #[conrod(common_builder)]
+    common: widget::CommonBuilder,
+}
+
+impl<'a> Overhead<'a> {
+    pub fn new(
+        name: &'a str,
+        stats: &'a Stats,
+        energy: &'a Energy,
+        own_level: u32,
+        pulse: f32,
+        imgs: &'a Imgs,
+        fonts: &'a ConrodVoxygenFonts,
+    ) -> Self {
+        Self {
+            name,
+            stats,
+            energy,
+            own_level,
+            pulse,
+            imgs,
+            fonts,
+            common: widget::CommonBuilder::default(),
+        }
+    }
+}
+
+pub struct State {
+    ids: Ids,
+}
+
+impl<'a> Ingameable for Overhead<'a> {
+    fn prim_count(&self) -> usize {
+        // Number of conrod primitives contained in the overhead display. TODO maybe
+        // this could be done automatically?
+        // - 2 Text::new for name
+        // - 2 Text::new for speech bubble
+        // - 10 Image::new for speech bubble (9-slice + tail)
+        // - 1 for level: either Text or Image
+        // - 4 for HP + mana + fg + bg
+        19
+    }
+}
+
+impl<'a> Widget for Overhead<'a> {
+    type Event = ();
+    type State = State;
+    type Style = ();
+
+    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
+        State {
+            ids: Ids::new(id_gen),
+        }
+    }
+
+    fn style(&self) -> Self::Style { () }
+
+    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
+        let widget::UpdateArgs { id, state, ui, .. } = args;
+
+        const BARSIZE: f64 = 2.0;
+        const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
+        const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
+
+        // Name
+        Text::new(&self.name)
+            .font_id(self.fonts.cyri.conrod_id)
+            .font_size(30)
+            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
+            .x_y(-1.0, MANA_BAR_Y + 48.0)
+            .set(state.ids.name_bg, ui);
+        Text::new(&self.name)
+            .font_id(self.fonts.cyri.conrod_id)
+            .font_size(30)
+            .color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
+            .x_y(0.0, MANA_BAR_Y + 50.0)
+            .set(state.ids.name, ui);
+
+        // Speech bubble
+        Text::new("Hello")
+            .font_id(self.fonts.cyri.conrod_id)
+            .font_size(15)
+            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
+            .up_from(state.ids.name, 10.0)
+            .x_align_to(state.ids.name, Align::Middle)
+            .parent(id)
+            .set(state.ids.chat_bubble_text, ui);
+        Image::new(self.imgs.chat_bubble_top_left)
+            .w_h(10.0, 10.0)
+            .top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_top_left, ui);
+        Image::new(self.imgs.chat_bubble_top)
+            .h(10.0)
+            .w_of(state.ids.chat_bubble_text)
+            .mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_top, ui);
+        Image::new(self.imgs.chat_bubble_top_right)
+            .w_h(10.0, 10.0)
+            .top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_top_right, ui);
+        Image::new(self.imgs.chat_bubble_left)
+            .w(10.0)
+            .h_of(state.ids.chat_bubble_text)
+            .mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_left, ui);
+        Image::new(self.imgs.chat_bubble_mid)
+            .wh_of(state.ids.chat_bubble_text)
+            .top_left_of(state.ids.chat_bubble_text)
+            .parent(id)
+            .set(state.ids.chat_bubble_mid, ui);
+        Image::new(self.imgs.chat_bubble_right)
+            .w(10.0)
+            .h_of(state.ids.chat_bubble_text)
+            .mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_right, ui);
+        Image::new(self.imgs.chat_bubble_bottom_left)
+            .w_h(10.0, 10.0)
+            .bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_bottom_left, ui);
+        Image::new(self.imgs.chat_bubble_bottom)
+            .h(10.0)
+            .w_of(state.ids.chat_bubble_text)
+            .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_bottom, ui);
+        Image::new(self.imgs.chat_bubble_bottom_right)
+            .w_h(10.0, 10.0)
+            .bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_bottom_right, ui);
+        Image::new(self.imgs.chat_bubble_tail)
+            .w_h(11.0, 16.0)
+            .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
+            .parent(id)
+            .set(state.ids.chat_bubble_tail, ui);
+        // Why is there a second text widget?: The first is to position the 9-slice
+        // around and the second is to display text. Changing .depth manually
+        // causes strange problems in unrelated parts of the ui (the debug
+        // overlay is offset by a npc's screen position) TODO
+        Text::new("Hello")
+            .font_id(self.fonts.cyri.conrod_id)
+            .font_size(15)
+            .top_left_of(state.ids.chat_bubble_text)
+            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
+            .parent(id)
+            .set(state.ids.chat_bubble_text2, ui);
+
+        let hp_percentage =
+            self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
+        let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
+        let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
+        let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
+
+        // Background
+        Image::new(self.imgs.enemy_health_bg)
+            .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
+            .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
+            .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
+            .parent(id)
+            .set(state.ids.health_bar_bg, ui);
+
+        // % HP Filling
+        Image::new(self.imgs.enemy_bar)
+            .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
+            .x_y(
+                (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
+                MANA_BAR_Y + 7.5,
+            )
+            .color(Some(if hp_percentage <= 25.0 {
+                crit_hp_color
+            } else if hp_percentage <= 50.0 {
+                LOW_HP_COLOR
+            } else {
+                HP_COLOR
+            }))
+            .parent(id)
+            .set(state.ids.health_bar, ui);
+        // % Mana Filling
+        Rectangle::fill_with(
+            [
+                72.0 * (self.energy.current() as f64 / self.energy.maximum() as f64) * BARSIZE,
+                MANA_BAR_HEIGHT,
+            ],
+            MANA_COLOR,
+        )
+        .x_y(
+            ((3.5 + (energy_percentage / 100.0 * 36.5)) - 36.45) * BARSIZE,
+            MANA_BAR_Y, //-32.0,
+        )
+        .parent(id)
+        .set(state.ids.mana_bar, ui);
+
+        // Foreground
+        Image::new(self.imgs.enemy_health)
+            .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
+            .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
+            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
+            .parent(id)
+            .set(state.ids.health_bar_fg, ui);
+
+        // Level
+        const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
+        const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
+        const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
+        // Change visuals of the level display depending on the player level/opponent
+        // level
+        let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
+        // + 10 level above player -> skull
+        // + 5-10 levels above player -> high
+        // -5 - +5 levels around player level -> equal
+        // - 5 levels below player -> low
+        if level_comp > 9 {
+            let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
+            Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
+                self.imgs.skull_2
+            } else {
+                self.imgs.skull
+            })
+            .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
+            .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
+            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
+            .parent(id)
+            .set(state.ids.level_skull, ui);
+        } else {
+            Text::new(&format!("{}", self.stats.level.level()))
+                .font_id(self.fonts.cyri.conrod_id)
+                .font_size(if self.stats.level.level() > 9 && level_comp < 10 {
+                    14
+                } else {
+                    15
+                })
+                .color(if level_comp > 4 {
+                    HIGH
+                } else if level_comp < -5 {
+                    LOW
+                } else {
+                    EQUAL
+                })
+                .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
+                .parent(id)
+                .set(state.ids.level, ui);
+        }
+    }
+}

From c65967ccdbf991cc15410047f5c788ce59bd03f1 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Sun, 24 May 2020 18:18:41 -0400
Subject: [PATCH 2/7] Chatting now creates speech bubbles

---
 common/src/comp/agent.rs        |  18 +++-
 common/src/comp/mod.rs          |   2 +-
 common/src/msg/ecs_packet.rs    |   7 ++
 common/src/state.rs             |   1 +
 server/src/lib.rs               |   1 +
 server/src/sys/message.rs       |  31 ++++---
 server/src/sys/mod.rs           |   4 +
 server/src/sys/sentinel.rs      |  17 +++-
 server/src/sys/speech_bubble.rs |  30 ++++++
 voxygen/src/hud/mod.rs          |  12 ++-
 voxygen/src/hud/overhead.rs     | 158 ++++++++++++++++----------------
 11 files changed, 184 insertions(+), 97 deletions(-)
 create mode 100644 server/src/sys/speech_bubble.rs

diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index be33b4bb49..a98c38e2f8 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -1,5 +1,5 @@
-use crate::path::Chaser;
-use specs::{Component, Entity as EcsEntity};
+use crate::{path::Chaser, state::Time};
+use specs::{Component, Entity as EcsEntity, FlaggedStorage, HashMapStorage};
 use specs_idvs::IDVStorage;
 use vek::*;
 
@@ -85,3 +85,17 @@ impl Activity {
 impl Default for Activity {
     fn default() -> Self { Activity::Idle(Vec2::zero()) }
 }
+
+/// Default duration in seconds of chat bubbles
+pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
+
+/// Adds a speech bubble to the entity
+#[derive(Clone, Default, Debug, Serialize, Deserialize)]
+pub struct SpeechBubble {
+    pub message: String,
+    pub timeout: Option<Time>,
+    // TODO add icon enum for player chat type / npc quest+trade
+}
+impl Component for SpeechBubble {
+    type Storage = FlaggedStorage<Self, HashMapStorage<Self>>;
+}
diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs
index 5bf573e9f0..dfea20e7ca 100644
--- a/common/src/comp/mod.rs
+++ b/common/src/comp/mod.rs
@@ -18,7 +18,7 @@ mod visual;
 // Reexports
 pub use ability::{CharacterAbility, ItemConfig, Loadout};
 pub use admin::Admin;
-pub use agent::{Agent, Alignment};
+pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
 pub use body::{
     biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem,
     humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs
index 2e7ad564c5..83ad59777f 100644
--- a/common/src/msg/ecs_packet.rs
+++ b/common/src/msg/ecs_packet.rs
@@ -24,6 +24,7 @@ sum_type! {
         Sticky(comp::Sticky),
         Loadout(comp::Loadout),
         CharacterState(comp::CharacterState),
+        SpeechBubble(comp::SpeechBubble),
         Pos(comp::Pos),
         Vel(comp::Vel),
         Ori(comp::Ori),
@@ -50,6 +51,7 @@ sum_type! {
         Sticky(PhantomData<comp::Sticky>),
         Loadout(PhantomData<comp::Loadout>),
         CharacterState(PhantomData<comp::CharacterState>),
+        SpeechBubble(PhantomData<comp::SpeechBubble>),
         Pos(PhantomData<comp::Pos>),
         Vel(PhantomData<comp::Vel>),
         Ori(PhantomData<comp::Ori>),
@@ -76,6 +78,7 @@ impl sync::CompPacket for EcsCompPacket {
             EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
             EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world),
             EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
+            EcsCompPacket::SpeechBubble(comp) => sync::handle_insert(comp, entity, world),
             EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world),
             EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world),
             EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world),
@@ -100,6 +103,7 @@ impl sync::CompPacket for EcsCompPacket {
             EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
             EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world),
             EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
+            EcsCompPacket::SpeechBubble(comp) => sync::handle_modify(comp, entity, world),
             EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world),
             EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
             EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world),
@@ -128,6 +132,9 @@ impl sync::CompPacket for EcsCompPacket {
             EcsCompPhantom::CharacterState(_) => {
                 sync::handle_remove::<comp::CharacterState>(entity, world)
             },
+            EcsCompPhantom::SpeechBubble(_) => {
+                sync::handle_remove::<comp::SpeechBubble>(entity, world)
+            },
             EcsCompPhantom::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world),
             EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world),
             EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(entity, world),
diff --git a/common/src/state.rs b/common/src/state.rs
index 9d47b9bbb6..b48d4ad35a 100644
--- a/common/src/state.rs
+++ b/common/src/state.rs
@@ -122,6 +122,7 @@ impl State {
         ecs.register::<comp::Sticky>();
         ecs.register::<comp::Gravity>();
         ecs.register::<comp::CharacterState>();
+        ecs.register::<comp::SpeechBubble>();
 
         // Register components send from clients -> server
         ecs.register::<comp::Controller>();
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 10fdb50740..2cec36ad00 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -109,6 +109,7 @@ impl Server {
         state.ecs_mut().insert(sys::TerrainSyncTimer::default());
         state.ecs_mut().insert(sys::TerrainTimer::default());
         state.ecs_mut().insert(sys::WaypointTimer::default());
+        state.ecs_mut().insert(sys::SpeechBubbleTimer::default());
         state
             .ecs_mut()
             .insert(sys::StatsPersistenceTimer::default());
diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs
index 8307c97d41..1579ccb477 100644
--- a/server/src/sys/message.rs
+++ b/server/src/sys/message.rs
@@ -4,7 +4,10 @@ use crate::{
     CLIENT_TIMEOUT,
 };
 use common::{
-    comp::{Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel},
+    comp::{
+        Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, SpeechBubble,
+        Stats, Vel, SPEECH_BUBBLE_DURATION,
+    },
     event::{EventBus, ServerEvent},
     msg::{
         validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate,
@@ -43,6 +46,7 @@ impl<'a> System<'a> for Sys {
         WriteStorage<'a, Player>,
         WriteStorage<'a, Client>,
         WriteStorage<'a, Controller>,
+        WriteStorage<'a, SpeechBubble>,
     );
 
     fn run(
@@ -67,6 +71,7 @@ impl<'a> System<'a> for Sys {
             mut players,
             mut clients,
             mut controllers,
+            mut speech_bubbles,
         ): Self::SystemData,
     ) {
         timer.start();
@@ -394,13 +399,20 @@ impl<'a> System<'a> for Sys {
         for (entity, msg) in new_chat_msgs {
             match msg {
                 ServerMsg::ChatMsg { chat_type, message } => {
-                    if let Some(entity) = entity {
+                    let message = if let Some(entity) = entity {
                         // Handle chat commands.
                         if message.starts_with("/") && message.len() > 1 {
                             let argv = String::from(&message[1..]);
                             server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
+                            continue;
                         } else {
-                            let message = match players.get(entity) {
+                            let timeout = Some(Time(time + SPEECH_BUBBLE_DURATION));
+                            let bubble = SpeechBubble {
+                                message: message.clone(),
+                                timeout,
+                            };
+                            let _ = speech_bubbles.insert(entity, bubble);
+                            match players.get(entity) {
                                 Some(player) => {
                                     if admins.get(entity).is_some() {
                                         format!("[ADMIN][{}] {}", &player.alias, message)
@@ -409,17 +421,14 @@ impl<'a> System<'a> for Sys {
                                     }
                                 },
                                 None => format!("[<Unknown>] {}", message),
-                            };
-                            let msg = ServerMsg::ChatMsg { chat_type, message };
-                            for client in (&mut clients).join().filter(|c| c.is_registered()) {
-                                client.notify(msg.clone());
                             }
                         }
                     } else {
-                        let msg = ServerMsg::ChatMsg { chat_type, message };
-                        for client in (&mut clients).join().filter(|c| c.is_registered()) {
-                            client.notify(msg.clone());
-                        }
+                        message
+                    };
+                    let msg = ServerMsg::ChatMsg { chat_type, message };
+                    for client in (&mut clients).join().filter(|c| c.is_registered()) {
+                        client.notify(msg.clone());
                     }
                 },
                 _ => {
diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs
index 6035fea593..e18e853ad8 100644
--- a/server/src/sys/mod.rs
+++ b/server/src/sys/mod.rs
@@ -2,6 +2,7 @@ pub mod entity_sync;
 pub mod message;
 pub mod persistence;
 pub mod sentinel;
+pub mod speech_bubble;
 pub mod subscription;
 pub mod terrain;
 pub mod terrain_sync;
@@ -20,6 +21,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
 pub type TerrainTimer = SysTimer<terrain::Sys>;
 pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
 pub type WaypointTimer = SysTimer<waypoint::Sys>;
+pub type SpeechBubbleTimer = SysTimer<speech_bubble::Sys>;
 pub type StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
 pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
 
@@ -31,11 +33,13 @@ pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
 //const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
 const TERRAIN_SYS: &str = "server_terrain_sys";
 const WAYPOINT_SYS: &str = "waypoint_sys";
+const SPEECH_BUBBLE_SYS: &str = "speech_bubble_sys";
 const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys";
 
 pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
     dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
     dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
+    dispatch_builder.add(speech_bubble::Sys, SPEECH_BUBBLE_SYS, &[]);
     dispatch_builder.add(persistence::stats::Sys, STATS_PERSISTENCE_SYS, &[]);
 }
 
diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs
index 3c90aec221..27acbd7a0f 100644
--- a/server/src/sys/sentinel.rs
+++ b/server/src/sys/sentinel.rs
@@ -2,7 +2,7 @@ use super::SysTimer;
 use common::{
     comp::{
         Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, Loadout,
-        Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
+        Mass, MountState, Mounting, Ori, Player, Pos, Scale, SpeechBubble, Stats, Sticky, Vel,
     },
     msg::EcsCompPacket,
     sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt},
@@ -54,6 +54,7 @@ pub struct TrackedComps<'a> {
     pub gravity: ReadStorage<'a, Gravity>,
     pub loadout: ReadStorage<'a, Loadout>,
     pub character_state: ReadStorage<'a, CharacterState>,
+    pub speech_bubble: ReadStorage<'a, SpeechBubble>,
 }
 impl<'a> TrackedComps<'a> {
     pub fn create_entity_package(
@@ -125,6 +126,10 @@ impl<'a> TrackedComps<'a> {
             .get(entity)
             .cloned()
             .map(|c| comps.push(c.into()));
+        self.speech_bubble
+            .get(entity)
+            .cloned()
+            .map(|c| comps.push(c.into()));
         // Add untracked comps
         pos.map(|c| comps.push(c.into()));
         vel.map(|c| comps.push(c.into()));
@@ -152,6 +157,7 @@ pub struct ReadTrackers<'a> {
     pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
     pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>,
     pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
+    pub speech_bubble: ReadExpect<'a, UpdateTracker<SpeechBubble>>,
 }
 impl<'a> ReadTrackers<'a> {
     pub fn create_sync_packages(
@@ -188,6 +194,12 @@ impl<'a> ReadTrackers<'a> {
                 &*self.character_state,
                 &comps.character_state,
                 filter,
+            )
+            .with_component(
+                &comps.uid,
+                &*self.speech_bubble,
+                &comps.speech_bubble,
+                filter,
             );
 
         (entity_sync_package, comp_sync_package)
@@ -213,6 +225,7 @@ pub struct WriteTrackers<'a> {
     gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
     loadout: WriteExpect<'a, UpdateTracker<Loadout>>,
     character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
+    speech_bubble: WriteExpect<'a, UpdateTracker<SpeechBubble>>,
 }
 
 fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@@ -236,6 +249,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
     trackers
         .character_state
         .record_changes(&comps.character_state);
+    trackers.speech_bubble.record_changes(&comps.speech_bubble);
 }
 
 pub fn register_trackers(world: &mut World) {
@@ -256,6 +270,7 @@ pub fn register_trackers(world: &mut World) {
     world.register_tracker::<Gravity>();
     world.register_tracker::<Loadout>();
     world.register_tracker::<CharacterState>();
+    world.register_tracker::<SpeechBubble>();
 }
 
 /// Deleted entities grouped by region
diff --git a/server/src/sys/speech_bubble.rs b/server/src/sys/speech_bubble.rs
new file mode 100644
index 0000000000..0af465498f
--- /dev/null
+++ b/server/src/sys/speech_bubble.rs
@@ -0,0 +1,30 @@
+use super::SysTimer;
+use common::{comp::SpeechBubble, state::Time};
+use specs::{Entities, Join, Read, System, Write, WriteStorage};
+
+/// This system removes timed-out speech bubbles
+pub struct Sys;
+impl<'a> System<'a> for Sys {
+    type SystemData = (
+        Entities<'a>,
+        Read<'a, Time>,
+        WriteStorage<'a, SpeechBubble>,
+        Write<'a, SysTimer<Self>>,
+    );
+
+    fn run(&mut self, (entities, time, mut speech_bubbles, mut timer): Self::SystemData) {
+        timer.start();
+
+        let expired_ents: Vec<_> = (&entities, &mut speech_bubbles)
+            .join()
+            .filter(|(_, speech_bubble)| speech_bubble.timeout.map_or(true, |t| t.0 < time.0))
+            .map(|(ent, _)| ent)
+            .collect();
+        for ent in expired_ents {
+            println!("Remoaving bobble");
+            speech_bubbles.remove(ent);
+        }
+
+        timer.end();
+    }
+}
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index f4743ec34f..f6bd1afdf3 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -566,6 +566,7 @@ impl Hud {
             let stats = ecs.read_storage::<comp::Stats>();
             let energy = ecs.read_storage::<comp::Energy>();
             let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
+            let speech_bubbles = ecs.read_storage::<comp::SpeechBubble>();
             let interpolated = ecs.read_storage::<vcomp::Interpolated>();
             let players = ecs.read_storage::<comp::Player>();
             let scales = ecs.read_storage::<comp::Scale>();
@@ -890,7 +891,7 @@ impl Hud {
             let mut sct_bg_walker = self.ids.sct_bgs.walk();
 
             // Render overhead name tags and health bars
-            for (pos, name, stats, energy, height_offset, hpfl) in (
+            for (pos, name, stats, energy, height_offset, hpfl, bubble) in (
                 &entities,
                 &pos,
                 interpolated.maybe(),
@@ -900,11 +901,12 @@ impl Hud {
                 scales.maybe(),
                 &bodies,
                 &hp_floater_lists,
+                speech_bubbles.maybe(),
             )
                 .join()
-                .filter(|(entity, _, _, stats, _, _, _, _, _)| *entity != me && !stats.is_dead)
+                .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
                 // Don't show outside a certain range
-                .filter(|(_, pos, _, _, _, _, _, _, hpfl)| {
+                .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| {
                     pos.0.distance_squared(player_pos)
                         < (if hpfl
                             .time_since_last_dmg_by_me
@@ -916,7 +918,7 @@ impl Hud {
                         })
                         .powi(2)
                 })
-                .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl)| {
+                .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, bubble)| {
                     // TODO: This is temporary
                     // If the player used the default character name display their name instead
                     let name = if stats.name == "Character Name" {
@@ -932,6 +934,7 @@ impl Hud {
                         // TODO: when body.height() is more accurate remove the 2.0
                         body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
                         hpfl,
+                        bubble,
                     )
                 })
             {
@@ -944,6 +947,7 @@ impl Hud {
                 // Chat bubble, name, level, and hp bars
                 overhead::Overhead::new(
                     &name,
+                    bubble,
                     stats,
                     energy,
                     own_level,
diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs
index 2f66aa5493..e46c07423e 100644
--- a/voxygen/src/hud/overhead.rs
+++ b/voxygen/src/hud/overhead.rs
@@ -1,6 +1,6 @@
 use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
 use crate::ui::{fonts::ConrodVoxygenFonts, Ingameable};
-use common::comp::{Energy, Stats};
+use common::comp::{Energy, SpeechBubble, Stats};
 use conrod_core::{
     position::Align,
     widget::{self, Image, Rectangle, Text},
@@ -42,6 +42,7 @@ widget_ids! {
 #[derive(WidgetCommon)]
 pub struct Overhead<'a> {
     name: &'a str,
+    bubble: Option<&'a SpeechBubble>,
     stats: &'a Stats,
     energy: &'a Energy,
     own_level: u32,
@@ -55,6 +56,7 @@ pub struct Overhead<'a> {
 impl<'a> Overhead<'a> {
     pub fn new(
         name: &'a str,
+        bubble: Option<&'a SpeechBubble>,
         stats: &'a Stats,
         energy: &'a Energy,
         own_level: u32,
@@ -64,6 +66,7 @@ impl<'a> Overhead<'a> {
     ) -> Self {
         Self {
             name,
+            bubble,
             stats,
             energy,
             own_level,
@@ -84,11 +87,12 @@ impl<'a> Ingameable for Overhead<'a> {
         // Number of conrod primitives contained in the overhead display. TODO maybe
         // this could be done automatically?
         // - 2 Text::new for name
-        // - 2 Text::new for speech bubble
-        // - 10 Image::new for speech bubble (9-slice + tail)
         // - 1 for level: either Text or Image
         // - 4 for HP + mana + fg + bg
-        19
+        // If there's a speech bubble
+        // - 1 Text::new for speech bubble
+        // - 10 Image::new for speech bubble (9-slice + tail)
+        7 + if self.bubble.is_some() { 11 } else { 0 }
     }
 }
 
@@ -126,80 +130,78 @@ impl<'a> Widget for Overhead<'a> {
             .x_y(0.0, MANA_BAR_Y + 50.0)
             .set(state.ids.name, ui);
 
-        // Speech bubble
-        Text::new("Hello")
-            .font_id(self.fonts.cyri.conrod_id)
-            .font_size(15)
-            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
-            .up_from(state.ids.name, 10.0)
-            .x_align_to(state.ids.name, Align::Middle)
-            .parent(id)
-            .set(state.ids.chat_bubble_text, ui);
-        Image::new(self.imgs.chat_bubble_top_left)
-            .w_h(10.0, 10.0)
-            .top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_top_left, ui);
-        Image::new(self.imgs.chat_bubble_top)
-            .h(10.0)
-            .w_of(state.ids.chat_bubble_text)
-            .mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_top, ui);
-        Image::new(self.imgs.chat_bubble_top_right)
-            .w_h(10.0, 10.0)
-            .top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_top_right, ui);
-        Image::new(self.imgs.chat_bubble_left)
-            .w(10.0)
-            .h_of(state.ids.chat_bubble_text)
-            .mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_left, ui);
-        Image::new(self.imgs.chat_bubble_mid)
-            .wh_of(state.ids.chat_bubble_text)
-            .top_left_of(state.ids.chat_bubble_text)
-            .parent(id)
-            .set(state.ids.chat_bubble_mid, ui);
-        Image::new(self.imgs.chat_bubble_right)
-            .w(10.0)
-            .h_of(state.ids.chat_bubble_text)
-            .mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_right, ui);
-        Image::new(self.imgs.chat_bubble_bottom_left)
-            .w_h(10.0, 10.0)
-            .bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_bottom_left, ui);
-        Image::new(self.imgs.chat_bubble_bottom)
-            .h(10.0)
-            .w_of(state.ids.chat_bubble_text)
-            .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_bottom, ui);
-        Image::new(self.imgs.chat_bubble_bottom_right)
-            .w_h(10.0, 10.0)
-            .bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_bottom_right, ui);
-        Image::new(self.imgs.chat_bubble_tail)
-            .w_h(11.0, 16.0)
-            .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
-            .parent(id)
-            .set(state.ids.chat_bubble_tail, ui);
-        // Why is there a second text widget?: The first is to position the 9-slice
-        // around and the second is to display text. Changing .depth manually
-        // causes strange problems in unrelated parts of the ui (the debug
-        // overlay is offset by a npc's screen position) TODO
-        Text::new("Hello")
-            .font_id(self.fonts.cyri.conrod_id)
-            .font_size(15)
-            .top_left_of(state.ids.chat_bubble_text)
-            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
-            .parent(id)
-            .set(state.ids.chat_bubble_text2, ui);
+        if let Some(bubble) = self.bubble {
+            // Speech bubble
+            let mut text = Text::new(&bubble.message)
+                .font_id(self.fonts.cyri.conrod_id)
+                .font_size(18)
+                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
+                .up_from(state.ids.name, 10.0)
+                .x_align_to(state.ids.name, Align::Middle)
+                .parent(id);
+            if let Some(w) = text.get_w(ui) {
+                if w > 250.0 {
+                    text = text.w(250.0);
+                }
+            }
+            Image::new(self.imgs.chat_bubble_top_left)
+                .w_h(10.0, 10.0)
+                .top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_top_left, ui);
+            Image::new(self.imgs.chat_bubble_top)
+                .h(10.0)
+                .w_of(state.ids.chat_bubble_text)
+                .mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_top, ui);
+            Image::new(self.imgs.chat_bubble_top_right)
+                .w_h(10.0, 10.0)
+                .top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_top_right, ui);
+            Image::new(self.imgs.chat_bubble_left)
+                .w(10.0)
+                .h_of(state.ids.chat_bubble_text)
+                .mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_left, ui);
+            Image::new(self.imgs.chat_bubble_mid)
+                .wh_of(state.ids.chat_bubble_text)
+                .top_left_of(state.ids.chat_bubble_text)
+                .parent(id)
+                .set(state.ids.chat_bubble_mid, ui);
+            Image::new(self.imgs.chat_bubble_right)
+                .w(10.0)
+                .h_of(state.ids.chat_bubble_text)
+                .mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_right, ui);
+            Image::new(self.imgs.chat_bubble_bottom_left)
+                .w_h(10.0, 10.0)
+                .bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_bottom_left, ui);
+            Image::new(self.imgs.chat_bubble_bottom)
+                .h(10.0)
+                .w_of(state.ids.chat_bubble_text)
+                .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_bottom, ui);
+            Image::new(self.imgs.chat_bubble_bottom_right)
+                .w_h(10.0, 10.0)
+                .bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
+                .parent(id)
+                .set(state.ids.chat_bubble_bottom_right, ui);
+            let tail = Image::new(self.imgs.chat_bubble_tail)
+                .w_h(11.0, 16.0)
+                .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
+                .parent(id);
+            // Move text to front (conrod depth is lowest first; not a z-index)
+            tail.set(state.ids.chat_bubble_tail, ui);
+            text.depth(tail.get_depth() - 1.0)
+                .set(state.ids.chat_bubble_text, ui);
+        }
 
         let hp_percentage =
             self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;

From 3c07d02218a5055416f776ffcd90eb87b0ab4ae1 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Sun, 24 May 2020 21:29:47 -0400
Subject: [PATCH 3/7] Add a dark mode to speech bubbles; consistantly use
 'speech bubble' instead of 'chat bubble'

---
 .../element/frames/bubble_dark/bottom.png     |   3 +
 .../frames/bubble_dark/bottom_left.png        |   3 +
 .../frames/bubble_dark/bottom_right.png       |   3 +
 .../element/frames/bubble_dark/left.png       |   3 +
 .../element/frames/bubble_dark/mid.png        |   3 +
 .../element/frames/bubble_dark/right.png      |   3 +
 .../element/frames/bubble_dark/tail.png       |   3 +
 .../element/frames/bubble_dark/top.png        |   3 +
 .../element/frames/bubble_dark/top_left.png   |   3 +
 .../element/frames/bubble_dark/top_right.png  |   3 +
 assets/voxygen/i18n/en.ron                    |   1 +
 common/src/comp/agent.rs                      |   2 +-
 voxygen/src/hud/img_ids.rs                    |  33 ++-
 voxygen/src/hud/mod.rs                        |   7 +-
 voxygen/src/hud/overhead.rs                   | 193 +++++++++++-------
 voxygen/src/hud/settings_window.rs            |  47 ++++-
 voxygen/src/session.rs                        |   4 +
 voxygen/src/settings.rs                       |   2 +
 18 files changed, 227 insertions(+), 92 deletions(-)
 create mode 100644 assets/voxygen/element/frames/bubble_dark/bottom.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/bottom_left.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/bottom_right.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/left.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/mid.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/right.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/tail.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/top.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/top_left.png
 create mode 100644 assets/voxygen/element/frames/bubble_dark/top_right.png

diff --git a/assets/voxygen/element/frames/bubble_dark/bottom.png b/assets/voxygen/element/frames/bubble_dark/bottom.png
new file mode 100644
index 0000000000..5f2253a85f
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/bottom.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98a71bfeb1258b3cfba3528ce236fbd1f6cb6a41327ba176b43b02476606c7fc
+size 140
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_left.png b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
new file mode 100644
index 0000000000..e95dc19f4e
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:887ef372eddeff35cbd6e2d77e97b9220a51b9dd5814f1b9f780c4e64e5ea3b8
+size 193
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_right.png b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
new file mode 100644
index 0000000000..033fc658e3
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81b50d4071f07b24d0b82c37d1bb53e1fbf839feda632bc3419edfcbdf2337d9
+size 208
diff --git a/assets/voxygen/element/frames/bubble_dark/left.png b/assets/voxygen/element/frames/bubble_dark/left.png
new file mode 100644
index 0000000000..62033229c0
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2632608c1365465c4e6dcb19963acdeec6890658b45aae5031d085b067b8627f
+size 132
diff --git a/assets/voxygen/element/frames/bubble_dark/mid.png b/assets/voxygen/element/frames/bubble_dark/mid.png
new file mode 100644
index 0000000000..6ca7fbf351
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/mid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c21f661d44fcdff48c6579e69ae978654ae964d476ab24253c5dc9ab6f7cfed
+size 109
diff --git a/assets/voxygen/element/frames/bubble_dark/right.png b/assets/voxygen/element/frames/bubble_dark/right.png
new file mode 100644
index 0000000000..d92b45226d
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:39b8d3074ef4f73027492dc8a8dd04722f0e5bd9543e470c440695153b85d8f3
+size 132
diff --git a/assets/voxygen/element/frames/bubble_dark/tail.png b/assets/voxygen/element/frames/bubble_dark/tail.png
new file mode 100644
index 0000000000..4fa55e9887
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/tail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:96a54addc80798ca47b2b652e60928bfcb0c039486b3b9adf29c4538f8888172
+size 256
diff --git a/assets/voxygen/element/frames/bubble_dark/top.png b/assets/voxygen/element/frames/bubble_dark/top.png
new file mode 100644
index 0000000000..f6a2dda333
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/top.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a5da78e4c5e5b8556bb738d4a80debf64755a8e4f7c3d4790007b32e84bb285a
+size 141
diff --git a/assets/voxygen/element/frames/bubble_dark/top_left.png b/assets/voxygen/element/frames/bubble_dark/top_left.png
new file mode 100644
index 0000000000..0f139f145d
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/top_left.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:061afcd80c0d69bacdce77c322d92b575d281b951ef7051f24a150b9c049d0cf
+size 171
diff --git a/assets/voxygen/element/frames/bubble_dark/top_right.png b/assets/voxygen/element/frames/bubble_dark/top_right.png
new file mode 100644
index 0000000000..4fde80e6b4
--- /dev/null
+++ b/assets/voxygen/element/frames/bubble_dark/top_right.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1a57638ec421508453151c1549a7c7cf197f9d7db503ec6c2f2a60c4b2275f3
+size 210
diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron
index c2ce3c3618..7dcd1b08b1 100644
--- a/assets/voxygen/i18n/en.ron
+++ b/assets/voxygen/i18n/en.ron
@@ -238,6 +238,7 @@ Enjoy your stay in the World of Veloren."#,
         "hud.settings.cumulated_damage": "Cumulated Damage",
         "hud.settings.incoming_damage": "Incoming Damage",
         "hud.settings.cumulated_incoming_damage": "Cumulated Incoming Damage",
+        "hud.settings.speech_bubble_dark_mode": "Speech Bubble Dark Mode",
         "hud.settings.energybar_numbers": "Energybar Numbers",
         "hud.settings.values": "Values",
         "hud.settings.percentages": "Percentages",
diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index a98c38e2f8..90be10bb0b 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -86,7 +86,7 @@ impl Default for Activity {
     fn default() -> Self { Activity::Idle(Vec2::zero()) }
 }
 
-/// Default duration in seconds of chat bubbles
+/// Default duration in seconds of speech bubbles
 pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
 
 /// Adds a speech bubble to the entity
diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs
index bec188275e..9417722fe4 100644
--- a/voxygen/src/hud/img_ids.rs
+++ b/voxygen/src/hud/img_ids.rs
@@ -272,17 +272,28 @@ image_ids! {
         progress_frame: "voxygen.element.frames.progress_bar",
         progress: "voxygen.element.misc_bg.progress",
 
-        // Chat bubbles
-        chat_bubble_top_left: "voxygen.element.frames.bubble.top_left",
-        chat_bubble_top: "voxygen.element.frames.bubble.top",
-        chat_bubble_top_right: "voxygen.element.frames.bubble.top_right",
-        chat_bubble_left: "voxygen.element.frames.bubble.left",
-        chat_bubble_mid: "voxygen.element.frames.bubble.mid",
-        chat_bubble_right: "voxygen.element.frames.bubble.right",
-        chat_bubble_bottom_left: "voxygen.element.frames.bubble.bottom_left",
-        chat_bubble_bottom: "voxygen.element.frames.bubble.bottom",
-        chat_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right",
-        chat_bubble_tail: "voxygen.element.frames.bubble.tail",
+        // Speech bubbles
+        speech_bubble_top_left: "voxygen.element.frames.bubble.top_left",
+        speech_bubble_top: "voxygen.element.frames.bubble.top",
+        speech_bubble_top_right: "voxygen.element.frames.bubble.top_right",
+        speech_bubble_left: "voxygen.element.frames.bubble.left",
+        speech_bubble_mid: "voxygen.element.frames.bubble.mid",
+        speech_bubble_right: "voxygen.element.frames.bubble.right",
+        speech_bubble_bottom_left: "voxygen.element.frames.bubble.bottom_left",
+        speech_bubble_bottom: "voxygen.element.frames.bubble.bottom",
+        speech_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right",
+        speech_bubble_tail: "voxygen.element.frames.bubble.tail",
+
+        dark_bubble_top_left: "voxygen.element.frames.bubble_dark.top_left",
+        dark_bubble_top: "voxygen.element.frames.bubble_dark.top",
+        dark_bubble_top_right: "voxygen.element.frames.bubble_dark.top_right",
+        dark_bubble_left: "voxygen.element.frames.bubble_dark.left",
+        dark_bubble_mid: "voxygen.element.frames.bubble_dark.mid",
+        dark_bubble_right: "voxygen.element.frames.bubble_dark.right",
+        dark_bubble_bottom_left: "voxygen.element.frames.bubble_dark.bottom_left",
+        dark_bubble_bottom: "voxygen.element.frames.bubble_dark.bottom",
+        dark_bubble_bottom_right: "voxygen.element.frames.bubble_dark.bottom_right",
+        dark_bubble_tail: "voxygen.element.frames.bubble_dark.tail",
 
         <BlankGraphic>
         nothing: (),
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index f6bd1afdf3..eb2690aad6 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -235,6 +235,7 @@ pub enum Event {
     Sct(bool),
     SctPlayerBatch(bool),
     SctDamageBatch(bool),
+    SpeechBubbleDarkMode(bool),
     ToggleDebug(bool),
     UiScale(ScaleChange),
     CharacterSelection,
@@ -944,13 +945,14 @@ impl Hud {
                 );
                 let ingame_pos = pos + Vec3::unit_z() * height_offset;
 
-                // Chat bubble, name, level, and hp bars
+                // Speech bubble, name, level, and hp bars
                 overhead::Overhead::new(
                     &name,
                     bubble,
                     stats,
                     energy,
                     own_level,
+                    &global_state.settings.gameplay,
                     self.pulse,
                     &self.imgs,
                     &self.fonts,
@@ -1626,6 +1628,9 @@ impl Hud {
             .set(self.ids.settings_window, ui_widgets)
             {
                 match event {
+                    settings_window::Event::SpeechBubbleDarkMode(sbdm) => {
+                        events.push(Event::SpeechBubbleDarkMode(sbdm));
+                    },
                     settings_window::Event::Sct(sct) => {
                         events.push(Event::Sct(sct));
                     },
diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs
index e46c07423e..9466fcde50 100644
--- a/voxygen/src/hud/overhead.rs
+++ b/voxygen/src/hud/overhead.rs
@@ -1,5 +1,8 @@
 use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
-use crate::ui::{fonts::ConrodVoxygenFonts, Ingameable};
+use crate::{
+    settings::GameplaySettings,
+    ui::{fonts::ConrodVoxygenFonts, Ingameable},
+};
 use common::comp::{Energy, SpeechBubble, Stats};
 use conrod_core::{
     position::Align,
@@ -9,19 +12,19 @@ use conrod_core::{
 
 widget_ids! {
     struct Ids {
-        // Chat bubble
-        chat_bubble_text,
-        chat_bubble_text2,
-        chat_bubble_top_left,
-        chat_bubble_top,
-        chat_bubble_top_right,
-        chat_bubble_left,
-        chat_bubble_mid,
-        chat_bubble_right,
-        chat_bubble_bottom_left,
-        chat_bubble_bottom,
-        chat_bubble_bottom_right,
-        chat_bubble_tail,
+        // Speech bubble
+        speech_bubble_text,
+        speech_bubble_text2,
+        speech_bubble_top_left,
+        speech_bubble_top,
+        speech_bubble_top_right,
+        speech_bubble_left,
+        speech_bubble_mid,
+        speech_bubble_right,
+        speech_bubble_bottom_left,
+        speech_bubble_bottom,
+        speech_bubble_bottom_right,
+        speech_bubble_tail,
 
         // Name
         name_bg,
@@ -46,6 +49,7 @@ pub struct Overhead<'a> {
     stats: &'a Stats,
     energy: &'a Energy,
     own_level: u32,
+    settings: &'a GameplaySettings,
     pulse: f32,
     imgs: &'a Imgs,
     fonts: &'a ConrodVoxygenFonts,
@@ -60,6 +64,7 @@ impl<'a> Overhead<'a> {
         stats: &'a Stats,
         energy: &'a Energy,
         own_level: u32,
+        settings: &'a GameplaySettings,
         pulse: f32,
         imgs: &'a Imgs,
         fonts: &'a ConrodVoxygenFonts,
@@ -70,6 +75,7 @@ impl<'a> Overhead<'a> {
             stats,
             energy,
             own_level,
+            settings,
             pulse,
             imgs,
             fonts,
@@ -130,77 +136,122 @@ impl<'a> Widget for Overhead<'a> {
             .x_y(0.0, MANA_BAR_Y + 50.0)
             .set(state.ids.name, ui);
 
+        // Speech bubble
         if let Some(bubble) = self.bubble {
-            // Speech bubble
+            let dark_mode = self.settings.speech_bubble_dark_mode;
             let mut text = Text::new(&bubble.message)
                 .font_id(self.fonts.cyri.conrod_id)
                 .font_size(18)
-                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
                 .up_from(state.ids.name, 10.0)
                 .x_align_to(state.ids.name, Align::Middle)
                 .parent(id);
+            text = if dark_mode {
+                text.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
+            } else {
+                text.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
+            };
             if let Some(w) = text.get_w(ui) {
                 if w > 250.0 {
                     text = text.w(250.0);
                 }
             }
-            Image::new(self.imgs.chat_bubble_top_left)
-                .w_h(10.0, 10.0)
-                .top_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_top_left, ui);
-            Image::new(self.imgs.chat_bubble_top)
-                .h(10.0)
-                .w_of(state.ids.chat_bubble_text)
-                .mid_top_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_top, ui);
-            Image::new(self.imgs.chat_bubble_top_right)
-                .w_h(10.0, 10.0)
-                .top_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_top_right, ui);
-            Image::new(self.imgs.chat_bubble_left)
-                .w(10.0)
-                .h_of(state.ids.chat_bubble_text)
-                .mid_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_left, ui);
-            Image::new(self.imgs.chat_bubble_mid)
-                .wh_of(state.ids.chat_bubble_text)
-                .top_left_of(state.ids.chat_bubble_text)
-                .parent(id)
-                .set(state.ids.chat_bubble_mid, ui);
-            Image::new(self.imgs.chat_bubble_right)
-                .w(10.0)
-                .h_of(state.ids.chat_bubble_text)
-                .mid_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_right, ui);
-            Image::new(self.imgs.chat_bubble_bottom_left)
-                .w_h(10.0, 10.0)
-                .bottom_left_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_bottom_left, ui);
-            Image::new(self.imgs.chat_bubble_bottom)
-                .h(10.0)
-                .w_of(state.ids.chat_bubble_text)
-                .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_bottom, ui);
-            Image::new(self.imgs.chat_bubble_bottom_right)
-                .w_h(10.0, 10.0)
-                .bottom_right_with_margin_on(state.ids.chat_bubble_text, -10.0)
-                .parent(id)
-                .set(state.ids.chat_bubble_bottom_right, ui);
-            let tail = Image::new(self.imgs.chat_bubble_tail)
-                .w_h(11.0, 16.0)
-                .mid_bottom_with_margin_on(state.ids.chat_bubble_text, -16.0)
-                .parent(id);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_top_left
+            } else {
+                self.imgs.speech_bubble_top_left
+            })
+            .w_h(10.0, 10.0)
+            .top_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_top_left, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_top
+            } else {
+                self.imgs.speech_bubble_top
+            })
+            .h(10.0)
+            .w_of(state.ids.speech_bubble_text)
+            .mid_top_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_top, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_top_right
+            } else {
+                self.imgs.speech_bubble_top_right
+            })
+            .w_h(10.0, 10.0)
+            .top_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_top_right, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_left
+            } else {
+                self.imgs.speech_bubble_left
+            })
+            .w(10.0)
+            .h_of(state.ids.speech_bubble_text)
+            .mid_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_left, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_mid
+            } else {
+                self.imgs.speech_bubble_mid
+            })
+            .wh_of(state.ids.speech_bubble_text)
+            .top_left_of(state.ids.speech_bubble_text)
+            .parent(id)
+            .set(state.ids.speech_bubble_mid, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_right
+            } else {
+                self.imgs.speech_bubble_right
+            })
+            .w(10.0)
+            .h_of(state.ids.speech_bubble_text)
+            .mid_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_right, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_bottom_left
+            } else {
+                self.imgs.speech_bubble_bottom_left
+            })
+            .w_h(10.0, 10.0)
+            .bottom_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_bottom_left, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_bottom
+            } else {
+                self.imgs.speech_bubble_bottom
+            })
+            .h(10.0)
+            .w_of(state.ids.speech_bubble_text)
+            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_bottom, ui);
+            Image::new(if dark_mode {
+                self.imgs.dark_bubble_bottom_right
+            } else {
+                self.imgs.speech_bubble_bottom_right
+            })
+            .w_h(10.0, 10.0)
+            .bottom_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .parent(id)
+            .set(state.ids.speech_bubble_bottom_right, ui);
+            let tail = Image::new(if dark_mode {
+                self.imgs.dark_bubble_tail
+            } else {
+                self.imgs.speech_bubble_tail
+            })
+            .w_h(11.0, 16.0)
+            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -16.0)
+            .parent(id);
             // Move text to front (conrod depth is lowest first; not a z-index)
-            tail.set(state.ids.chat_bubble_tail, ui);
+            tail.set(state.ids.speech_bubble_tail, ui);
             text.depth(tail.get_depth() - 1.0)
-                .set(state.ids.chat_bubble_text, ui);
+                .set(state.ids.speech_bubble_text, ui);
         }
 
         let hp_percentage =
diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs
index 4279d7b964..c4f5643878 100644
--- a/voxygen/src/hud/settings_window.rs
+++ b/voxygen/src/hud/settings_window.rs
@@ -153,6 +153,8 @@ widget_ids! {
         sct_num_dur_text,
         sct_num_dur_slider,
         sct_num_dur_value,
+        speech_bubble_dark_mode_text,
+        speech_bubble_dark_mode_button,
         free_look_behavior_text,
         free_look_behavior_list
     }
@@ -235,6 +237,7 @@ pub enum Event {
     Sct(bool),
     SctPlayerBatch(bool),
     SctDamageBatch(bool),
+    SpeechBubbleDarkMode(bool),
     ChangeLanguage(LanguageMetadata),
     ChangeBinding(GameInput),
     ChangeFreeLookBehavior(PressBehavior),
@@ -943,17 +946,45 @@ impl<'a> Widget for SettingsWindow<'a> {
                 .set(state.ids.sct_batch_inc_text, ui);
             }
 
+            // Speech bubble dark mode
+            let speech_bubble_dark_mode = ToggleButton::new(
+                self.global_state.settings.gameplay.speech_bubble_dark_mode,
+                self.imgs.checkbox,
+                self.imgs.checkbox_checked,
+            )
+            .down_from(
+                if self.global_state.settings.gameplay.sct {
+                    state.ids.sct_batch_inc_radio
+                } else {
+                    state.ids.sct_show_radio
+                },
+                20.0,
+            )
+            .x(0.0)
+            .w_h(18.0, 18.0)
+            .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
+            .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
+            .set(state.ids.speech_bubble_dark_mode_button, ui);
+            if self.global_state.settings.gameplay.speech_bubble_dark_mode
+                != speech_bubble_dark_mode
+            {
+                events.push(Event::SpeechBubbleDarkMode(speech_bubble_dark_mode));
+            }
+            Text::new(
+                &self
+                    .localized_strings
+                    .get("hud.settings.speech_bubble_dark_mode"),
+            )
+            .right_from(state.ids.speech_bubble_dark_mode_button, 10.0)
+            .font_size(self.fonts.cyri.scale(18))
+            .font_id(self.fonts.cyri.conrod_id)
+            .color(TEXT_COLOR)
+            .set(state.ids.speech_bubble_dark_mode_text, ui);
+
             // Energybars Numbers
             // Hotbar text
             Text::new(&self.localized_strings.get("hud.settings.energybar_numbers"))
-                .down_from(
-                    if self.global_state.settings.gameplay.sct {
-                        state.ids.sct_batch_inc_radio
-                    } else {
-                        state.ids.sct_show_radio
-                    },
-                    20.0,
-                )
+                .down_from(state.ids.speech_bubble_dark_mode_button, 20.0)
                 .font_size(self.fonts.cyri.scale(18))
                 .font_id(self.fonts.cyri.conrod_id)
                 .color(TEXT_COLOR)
diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs
index f8ac11ef23..e785de0d9c 100644
--- a/voxygen/src/session.rs
+++ b/voxygen/src/session.rs
@@ -594,6 +594,10 @@ impl PlayState for SessionState {
                         global_state.settings.gameplay.sct_damage_batch = sct_damage_batch;
                         global_state.settings.save_to_file_warn();
                     },
+                    HudEvent::SpeechBubbleDarkMode(sbdm) => {
+                        global_state.settings.gameplay.speech_bubble_dark_mode = sbdm;
+                        global_state.settings.save_to_file_warn();
+                    },
                     HudEvent::ToggleDebug(toggle_debug) => {
                         global_state.settings.gameplay.toggle_debug = toggle_debug;
                         global_state.settings.save_to_file_warn();
diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs
index 8b76af4a4a..5ae6d87294 100644
--- a/voxygen/src/settings.rs
+++ b/voxygen/src/settings.rs
@@ -455,6 +455,7 @@ pub struct GameplaySettings {
     pub sct: bool,
     pub sct_player_batch: bool,
     pub sct_damage_batch: bool,
+    pub speech_bubble_dark_mode: bool,
     pub mouse_y_inversion: bool,
     pub smooth_pan_enable: bool,
     pub crosshair_transp: f32,
@@ -480,6 +481,7 @@ impl Default for GameplaySettings {
             sct: true,
             sct_player_batch: true,
             sct_damage_batch: false,
+            speech_bubble_dark_mode: false,
             crosshair_transp: 0.6,
             chat_transp: 0.4,
             crosshair_type: CrosshairType::Round,

From 3cea76b82f331e3ed5b28951756ffaa1a38a2705 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Mon, 25 May 2020 20:11:22 -0400
Subject: [PATCH 4/7] NPCs now call for help when you hit them. Redraw speech
 bubble dark mode.

---
 .../element/frames/bubble_dark/bottom.png     |  4 +-
 .../frames/bubble_dark/bottom_left.png        |  4 +-
 .../frames/bubble_dark/bottom_right.png       |  4 +-
 .../element/frames/bubble_dark/left.png       |  4 +-
 .../element/frames/bubble_dark/mid.png        |  2 +-
 .../element/frames/bubble_dark/right.png      |  4 +-
 .../element/frames/bubble_dark/tail.png       |  4 +-
 .../element/frames/bubble_dark/top.png        |  4 +-
 .../element/frames/bubble_dark/top_left.png   |  4 +-
 .../element/frames/bubble_dark/top_right.png  |  4 +-
 assets/voxygen/i18n/de_DE.ron                 |  5 +-
 assets/voxygen/i18n/en.ron                    | 44 ++++++++++++-
 assets/voxygen/i18n/fr_FR.ron                 |  5 +-
 assets/voxygen/i18n/it_IT.ron                 |  5 +-
 assets/voxygen/i18n/pt_PT.ron                 |  3 +
 assets/voxygen/i18n/ru_RU.ron                 |  3 +
 assets/voxygen/i18n/tr_TR.ron                 |  3 +
 common/src/comp/agent.rs                      | 37 ++++++++++-
 common/src/sys/agent.rs                       |  8 ++-
 server/src/sys/message.rs                     | 16 ++---
 server/src/sys/speech_bubble.rs               |  1 -
 voxygen/src/hud/mod.rs                        |  1 +
 voxygen/src/hud/overhead.rs                   | 10 ++-
 voxygen/src/i18n.rs                           | 64 +++++++++++++++----
 24 files changed, 191 insertions(+), 52 deletions(-)

diff --git a/assets/voxygen/element/frames/bubble_dark/bottom.png b/assets/voxygen/element/frames/bubble_dark/bottom.png
index 5f2253a85f..47fc0ae80b 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:98a71bfeb1258b3cfba3528ce236fbd1f6cb6a41327ba176b43b02476606c7fc
-size 140
+oid sha256:0649ffdee5c242f0e268de834dab7bacfcd6758cbb40fa30af6ef088b7bb1b88
+size 129
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_left.png b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
index e95dc19f4e..80e6afd9c3 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom_left.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:887ef372eddeff35cbd6e2d77e97b9220a51b9dd5814f1b9f780c4e64e5ea3b8
-size 193
+oid sha256:cbde3c25035fbb219ba954c748fb5ca3f5d57f9d0632ebb4a288238146e95a62
+size 180
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_right.png b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
index 033fc658e3..888a0514ef 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom_right.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:81b50d4071f07b24d0b82c37d1bb53e1fbf839feda632bc3419edfcbdf2337d9
-size 208
+oid sha256:aad2eb4c05eb6215c5641478c1ad8f7f5819e524c7fa5f610b1bc2568ebc978a
+size 185
diff --git a/assets/voxygen/element/frames/bubble_dark/left.png b/assets/voxygen/element/frames/bubble_dark/left.png
index 62033229c0..2a726a2cf3 100644
--- a/assets/voxygen/element/frames/bubble_dark/left.png
+++ b/assets/voxygen/element/frames/bubble_dark/left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:2632608c1365465c4e6dcb19963acdeec6890658b45aae5031d085b067b8627f
-size 132
+oid sha256:048b2251d7f2970b8085c8792239f71888aa5344231620af1871ae3cad6fbb1e
+size 126
diff --git a/assets/voxygen/element/frames/bubble_dark/mid.png b/assets/voxygen/element/frames/bubble_dark/mid.png
index 6ca7fbf351..e5cf820008 100644
--- a/assets/voxygen/element/frames/bubble_dark/mid.png
+++ b/assets/voxygen/element/frames/bubble_dark/mid.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7c21f661d44fcdff48c6579e69ae978654ae964d476ab24253c5dc9ab6f7cfed
+oid sha256:4a7de055972cc48b200a54c8fc86c1e9470c74b20ef64fd0b4a5bf658601a823
 size 109
diff --git a/assets/voxygen/element/frames/bubble_dark/right.png b/assets/voxygen/element/frames/bubble_dark/right.png
index d92b45226d..f5682b6f24 100644
--- a/assets/voxygen/element/frames/bubble_dark/right.png
+++ b/assets/voxygen/element/frames/bubble_dark/right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:39b8d3074ef4f73027492dc8a8dd04722f0e5bd9543e470c440695153b85d8f3
-size 132
+oid sha256:35af73b41aa32b94276f143e3156bdca0151590162a610f7700f898011bdddfa
+size 131
diff --git a/assets/voxygen/element/frames/bubble_dark/tail.png b/assets/voxygen/element/frames/bubble_dark/tail.png
index 4fa55e9887..3cc41634fa 100644
--- a/assets/voxygen/element/frames/bubble_dark/tail.png
+++ b/assets/voxygen/element/frames/bubble_dark/tail.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:96a54addc80798ca47b2b652e60928bfcb0c039486b3b9adf29c4538f8888172
-size 256
+oid sha256:0b21fefeab508b57af56eaa72153cd584fa8f6db41f639778368b46d2a0f2f38
+size 221
diff --git a/assets/voxygen/element/frames/bubble_dark/top.png b/assets/voxygen/element/frames/bubble_dark/top.png
index f6a2dda333..01d893ab00 100644
--- a/assets/voxygen/element/frames/bubble_dark/top.png
+++ b/assets/voxygen/element/frames/bubble_dark/top.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:a5da78e4c5e5b8556bb738d4a80debf64755a8e4f7c3d4790007b32e84bb285a
-size 141
+oid sha256:59fba25aad27f065ee5e2446400a1d9ce1a98b59b100ab9328880d1c25f9031a
+size 129
diff --git a/assets/voxygen/element/frames/bubble_dark/top_left.png b/assets/voxygen/element/frames/bubble_dark/top_left.png
index 0f139f145d..c89832ec6f 100644
--- a/assets/voxygen/element/frames/bubble_dark/top_left.png
+++ b/assets/voxygen/element/frames/bubble_dark/top_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:061afcd80c0d69bacdce77c322d92b575d281b951ef7051f24a150b9c049d0cf
-size 171
+oid sha256:33c47f93a1641466781dea3fb5115ccb689ed80a210875032d7cddce481fce47
+size 169
diff --git a/assets/voxygen/element/frames/bubble_dark/top_right.png b/assets/voxygen/element/frames/bubble_dark/top_right.png
index 4fde80e6b4..14aeb65943 100644
--- a/assets/voxygen/element/frames/bubble_dark/top_right.png
+++ b/assets/voxygen/element/frames/bubble_dark/top_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e1a57638ec421508453151c1549a7c7cf197f9d7db503ec6c2f2a60c4b2275f3
-size 210
+oid sha256:60c4e98e21e2e738552833a080e80ab02bc61a5bbec99101d7c9b75c39caeb7c
+size 186
diff --git a/assets/voxygen/i18n/de_DE.ron b/assets/voxygen/i18n/de_DE.ron
index 7825275b69..630a118ef3 100644
--- a/assets/voxygen/i18n/de_DE.ron
+++ b/assets/voxygen/i18n/de_DE.ron
@@ -383,5 +383,8 @@ Willenskraft
         "esc_menu.logout": "Ausloggen",
         "esc_menu.quit_game": "Desktop",
         /// End Escape Menu Section
+    },
+
+    vector_map: {
     }
-)
\ No newline at end of file
+)
diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron
index 7dcd1b08b1..fef4e49ccb 100644
--- a/assets/voxygen/i18n/en.ron
+++ b/assets/voxygen/i18n/en.ron
@@ -376,14 +376,52 @@ Fitness
 
 Willpower
 "#,
-
-
-        /// Start character window section
+        /// End character window section
 
 
         /// Start Escape Menu Section
         "esc_menu.logout": "Logout",
         "esc_menu.quit_game": "Quit Game",
         /// End Escape Menu Section
+    },
+
+    vector_map: {
+        "npc.speech.villager_under_attack": [
+            "Help, I'm under attack!",
+            "Help! I'm under attack!",
+            "Ouch! I'm under attack!",
+            "Ouch! I'm under attack! Help!",
+            "Help me! I'm under attack!",
+            "I'm under attack! Help!",
+            "I'm under attack! Help me!",
+            "Help!",
+            "Help! Help!",
+            "Help! Help! Help!",
+            "I'm under attack!",
+            "AAAHHH! I'm under attack!",
+            "AAAHHH! I'm under attack! Help!",
+            "Help! We're under attack!",
+            "Help! Murderer!",
+            "Help! There's a murder on the loose!",
+            "Help! They're trying to kill me!",
+            "Guards, I'm under attack!",
+            "Guards! I'm under attack!",
+            "I'm under attack! Guards!",
+            "Help! Guards! I'm under attack!",
+            "Guards! Come quick!",
+            "Guards! Guards!",
+            "Guards! There's a villain attacking me!",
+            "Guards, slay this foul villain!",
+            "Guards! There's a murderer!",
+            "Guards! Help me!",
+            "You won't get away with this! Guards!",
+            "You fiend!",
+            "Help me!",
+            "Help! Please!",
+            "Ouch! Guards! Help!",
+            "They're coming for me!",
+            "Help! Help! I'm being repressed",
+            "Ah, now we see the violence inherent in the system.",
+        ],
     }
 )
diff --git a/assets/voxygen/i18n/fr_FR.ron b/assets/voxygen/i18n/fr_FR.ron
index 6d5c4d581c..89a2685dd4 100644
--- a/assets/voxygen/i18n/fr_FR.ron
+++ b/assets/voxygen/i18n/fr_FR.ron
@@ -323,5 +323,8 @@ Force
 Dexterité
 
 Intelligence"#,
+    },
+
+    vector_map: {
     }
-)
\ No newline at end of file
+)
diff --git a/assets/voxygen/i18n/it_IT.ron b/assets/voxygen/i18n/it_IT.ron
index 5af28e1e58..5a14c15a9b 100644
--- a/assets/voxygen/i18n/it_IT.ron
+++ b/assets/voxygen/i18n/it_IT.ron
@@ -524,5 +524,8 @@ Volontà
                 "esc_menu.logout": "Disconnettiti",
                 "esc_menu.quit_game": "Esci dal Gioco",
                 /// End Escape Menu Section
-            }
+    },
+
+    vector_map: {
+    }
 )
diff --git a/assets/voxygen/i18n/pt_PT.ron b/assets/voxygen/i18n/pt_PT.ron
index a29d45f147..c4832499a9 100644
--- a/assets/voxygen/i18n/pt_PT.ron
+++ b/assets/voxygen/i18n/pt_PT.ron
@@ -368,5 +368,8 @@ Força de vontade
         "esc_menu.logout": "Desconectar",
         "esc_menu.quit_game": "Sair do jogo",
         /// End Escape Menu Section
+    },
+
+    vector_map: {
     }
 )
diff --git a/assets/voxygen/i18n/ru_RU.ron b/assets/voxygen/i18n/ru_RU.ron
index 51ae278708..da55da407b 100644
--- a/assets/voxygen/i18n/ru_RU.ron
+++ b/assets/voxygen/i18n/ru_RU.ron
@@ -365,5 +365,8 @@ https://account.veloren.net."#,
         "esc_menu.logout": "Выйти в меню",
         "esc_menu.quit_game": "Выйти из игры",
         /// End Escape Menu Section
+    },
+
+    vector_map: {
     }
 )
diff --git a/assets/voxygen/i18n/tr_TR.ron b/assets/voxygen/i18n/tr_TR.ron
index 17d62c5ce8..2f88227ad9 100644
--- a/assets/voxygen/i18n/tr_TR.ron
+++ b/assets/voxygen/i18n/tr_TR.ron
@@ -397,5 +397,8 @@ Hareket gücü
         "esc_menu.logout": "Çıkış yap",
         "esc_menu.quit_game": "Oyundan çık",
         /// End Escape Menu Section
+    },
+
+    vector_map: {
     }
 )
diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index 90be10bb0b..b0e3ecac60 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -89,13 +89,46 @@ impl Default for Activity {
 /// Default duration in seconds of speech bubbles
 pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
 
+/// The contents of a speech bubble
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum SpeechBubbleMessage {
+    /// This message was said by a player and needs no translation
+    Plain(String),
+    /// This message was said by an NPC. The fields are a i18n key and a random
+    /// u16 index
+    Localized(String, u16),
+}
+
 /// Adds a speech bubble to the entity
-#[derive(Clone, Default, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct SpeechBubble {
-    pub message: String,
+    pub message: SpeechBubbleMessage,
     pub timeout: Option<Time>,
     // TODO add icon enum for player chat type / npc quest+trade
 }
 impl Component for SpeechBubble {
     type Storage = FlaggedStorage<Self, HashMapStorage<Self>>;
 }
+impl SpeechBubble {
+    pub fn npc_new(i18n_key: String, now: Time) -> Self {
+        let message = SpeechBubbleMessage::Localized(i18n_key, rand::random());
+        let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
+        Self { message, timeout }
+    }
+
+    pub fn player_new(message: String, now: Time) -> Self {
+        let message = SpeechBubbleMessage::Plain(message);
+        let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
+        Self { message, timeout }
+    }
+
+    pub fn message<F>(&self, i18n_variation: F) -> String
+    where
+        F: Fn(String, u16) -> String,
+    {
+        match &self.message {
+            SpeechBubbleMessage::Plain(m) => m.to_string(),
+            SpeechBubbleMessage::Localized(k, i) => i18n_variation(k.to_string(), *i).to_string(),
+        }
+    }
+}
diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs
index 9f349ada3c..70015af7c8 100644
--- a/common/src/sys/agent.rs
+++ b/common/src/sys/agent.rs
@@ -4,7 +4,7 @@ use crate::{
         agent::Activity,
         item::{tool::ToolKind, ItemKind},
         Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos,
-        Scale, Stats,
+        Scale, SpeechBubble, Stats,
     },
     path::Chaser,
     state::{DeltaTime, Time},
@@ -38,6 +38,7 @@ impl<'a> System<'a> for Sys {
         ReadStorage<'a, Alignment>,
         WriteStorage<'a, Agent>,
         WriteStorage<'a, Controller>,
+        WriteStorage<'a, SpeechBubble>,
         ReadStorage<'a, MountState>,
     );
 
@@ -58,6 +59,7 @@ impl<'a> System<'a> for Sys {
             alignments,
             mut agents,
             mut controllers,
+            mut speech_bubbles,
             mount_states,
         ): Self::SystemData,
     ) {
@@ -386,6 +388,10 @@ impl<'a> System<'a> for Sys {
                         if !agent.activity.is_attack() {
                             if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
                             {
+                                let message = "npc.speech.villager_under_attack".to_string();
+                                let bubble = SpeechBubble::npc_new(message, *time);
+                                let _ = speech_bubbles.insert(entity, bubble);
+
                                 agent.activity = Activity::Attack {
                                     target: attacker,
                                     chaser: Chaser::default(),
diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs
index 1579ccb477..3e18372abd 100644
--- a/server/src/sys/message.rs
+++ b/server/src/sys/message.rs
@@ -6,7 +6,7 @@ use crate::{
 use common::{
     comp::{
         Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, SpeechBubble,
-        Stats, Vel, SPEECH_BUBBLE_DURATION,
+        Stats, Vel,
     },
     event::{EventBus, ServerEvent},
     msg::{
@@ -76,8 +76,6 @@ impl<'a> System<'a> for Sys {
     ) {
         timer.start();
 
-        let time = time.0;
-
         let persistence_db_dir = &persistence_db_dir.0;
 
         let mut server_emitter = server_event_bus.emitter();
@@ -97,13 +95,13 @@ impl<'a> System<'a> for Sys {
 
             // Update client ping.
             if new_msgs.len() > 0 {
-                client.last_ping = time
-            } else if time - client.last_ping > CLIENT_TIMEOUT // Timeout
+                client.last_ping = time.0
+            } else if time.0 - client.last_ping > CLIENT_TIMEOUT // Timeout
                 || client.postbox.error().is_some()
             // Postbox error
             {
                 server_emitter.emit(ServerEvent::ClientDisconnect(entity));
-            } else if time - client.last_ping > CLIENT_TIMEOUT * 0.5 {
+            } else if time.0 - client.last_ping > CLIENT_TIMEOUT * 0.5 {
                 // Try pinging the client if the timeout is nearing.
                 client.postbox.send_message(ServerMsg::Ping);
             }
@@ -406,11 +404,7 @@ impl<'a> System<'a> for Sys {
                             server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
                             continue;
                         } else {
-                            let timeout = Some(Time(time + SPEECH_BUBBLE_DURATION));
-                            let bubble = SpeechBubble {
-                                message: message.clone(),
-                                timeout,
-                            };
+                            let bubble = SpeechBubble::player_new(message.clone(), *time);
                             let _ = speech_bubbles.insert(entity, bubble);
                             match players.get(entity) {
                                 Some(player) => {
diff --git a/server/src/sys/speech_bubble.rs b/server/src/sys/speech_bubble.rs
index 0af465498f..2a3183fbaf 100644
--- a/server/src/sys/speech_bubble.rs
+++ b/server/src/sys/speech_bubble.rs
@@ -21,7 +21,6 @@ impl<'a> System<'a> for Sys {
             .map(|(ent, _)| ent)
             .collect();
         for ent in expired_ents {
-            println!("Remoaving bobble");
             speech_bubbles.remove(ent);
         }
 
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index eb2690aad6..8372f0a171 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -954,6 +954,7 @@ impl Hud {
                     own_level,
                     &global_state.settings.gameplay,
                     self.pulse,
+                    &self.voxygen_i18n,
                     &self.imgs,
                     &self.fonts,
                 )
diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs
index 9466fcde50..bfe14afd76 100644
--- a/voxygen/src/hud/overhead.rs
+++ b/voxygen/src/hud/overhead.rs
@@ -1,5 +1,6 @@
 use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR};
 use crate::{
+    i18n::VoxygenLocalization,
     settings::GameplaySettings,
     ui::{fonts::ConrodVoxygenFonts, Ingameable},
 };
@@ -51,6 +52,7 @@ pub struct Overhead<'a> {
     own_level: u32,
     settings: &'a GameplaySettings,
     pulse: f32,
+    voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
     imgs: &'a Imgs,
     fonts: &'a ConrodVoxygenFonts,
     #[conrod(common_builder)]
@@ -66,6 +68,7 @@ impl<'a> Overhead<'a> {
         own_level: u32,
         settings: &'a GameplaySettings,
         pulse: f32,
+        voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
         imgs: &'a Imgs,
         fonts: &'a ConrodVoxygenFonts,
     ) -> Self {
@@ -77,6 +80,7 @@ impl<'a> Overhead<'a> {
             own_level,
             settings,
             pulse,
+            voxygen_i18n,
             imgs,
             fonts,
             common: widget::CommonBuilder::default(),
@@ -139,7 +143,11 @@ impl<'a> Widget for Overhead<'a> {
         // Speech bubble
         if let Some(bubble) = self.bubble {
             let dark_mode = self.settings.speech_bubble_dark_mode;
-            let mut text = Text::new(&bubble.message)
+            let localizer =
+                |s: String, i| -> String { self.voxygen_i18n.get_variation(&s, i).to_string() };
+            let bubble_contents: String = bubble.message(localizer);
+
+            let mut text = Text::new(&bubble_contents)
                 .font_id(self.fonts.cyri.conrod_id)
                 .font_size(18)
                 .up_from(state.ids.name, 10.0)
diff --git a/voxygen/src/i18n.rs b/voxygen/src/i18n.rs
index 81b758a128..e38108bb3e 100644
--- a/voxygen/src/i18n.rs
+++ b/voxygen/src/i18n.rs
@@ -53,10 +53,15 @@ pub type VoxygenFonts = HashMap<String, Font>;
 pub struct VoxygenLocalization {
     /// A map storing the localized texts
     ///
-    /// Localized content can be access using a String key
+    /// Localized content can be accessed using a String key.
     pub string_map: HashMap<String, String>,
 
-    /// Either to convert the input text encoded in UTF-8
+    /// A map for storing variations of localized texts, for example multiple
+    /// ways of saying "Help, I'm under attack". Used primarily for npc
+    /// dialogue.
+    pub vector_map: HashMap<String, Vec<String>>,
+
+    /// Whether to convert the input text encoded in UTF-8
     /// into a ASCII version by using the `deunicode` crate.
     pub convert_utf8_to_ascii: bool,
 
@@ -78,23 +83,56 @@ impl VoxygenLocalization {
         }
     }
 
-    /// Return the missing keys compared to the reference language and return
-    /// them
-    pub fn list_missing_entries(&self) -> HashSet<String> {
+    /// Get a variation of localized text from the given key
+    ///
+    /// `index` should be a random number from `0` to `u16::max()`
+    ///
+    /// If the key is not present in the localization object
+    /// then the key is returned.
+    pub fn get_variation<'a>(&'a self, key: &'a str, index: u16) -> &str {
+        match self.vector_map.get(key) {
+            Some(v) if !v.is_empty() => &v[index as usize % v.len()],
+            _ => key,
+        }
+    }
+
+    /// Return the missing keys compared to the reference language
+    pub fn list_missing_entries(&self) -> (HashSet<String>, HashSet<String>) {
         let reference_localization =
             load_expect::<VoxygenLocalization>(i18n_asset_key(REFERENCE_LANG).as_ref());
-        let reference_keys: HashSet<_> =
-            reference_localization.string_map.keys().cloned().collect();
-        let current_keys: HashSet<_> = self.string_map.keys().cloned().collect();
 
-        reference_keys.difference(&current_keys).cloned().collect()
+        let reference_string_keys: HashSet<_> =
+            reference_localization.string_map.keys().cloned().collect();
+        let string_keys: HashSet<_> = self.string_map.keys().cloned().collect();
+        let strings = reference_string_keys
+            .difference(&string_keys)
+            .cloned()
+            .collect();
+
+        let reference_vector_keys: HashSet<_> =
+            reference_localization.vector_map.keys().cloned().collect();
+        let vector_keys: HashSet<_> = self.vector_map.keys().cloned().collect();
+        let vectors = reference_vector_keys
+            .difference(&vector_keys)
+            .cloned()
+            .collect();
+
+        (strings, vectors)
     }
 
     /// Log missing entries (compared to the reference language) as warnings
     pub fn log_missing_entries(&self) {
-        for missing_key in self.list_missing_entries() {
+        let (missing_strings, missing_vectors) = self.list_missing_entries();
+        for missing_key in missing_strings {
             log::warn!(
-                "[{:?}] Missing key {:?}",
+                "[{:?}] Missing string key {:?}",
+                self.metadata.language_identifier,
+                missing_key
+            );
+        }
+        for missing_key in missing_vectors {
+            log::warn!(
+                "[{:?}] Missing vector key {:?}",
                 self.metadata.language_identifier,
                 missing_key
             );
@@ -116,6 +154,10 @@ impl Asset for VoxygenLocalization {
             for value in asked_localization.string_map.values_mut() {
                 *value = deunicode(value);
             }
+
+            for value in asked_localization.vector_map.values_mut() {
+                *value = value.into_iter().map(|s| deunicode(s)).collect();
+            }
         }
         asked_localization.metadata.language_name =
             deunicode(&asked_localization.metadata.language_name);

From 78a06550d0bf6ae591ba7096903acd243e9c3f22 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Mon, 25 May 2020 22:45:13 -0400
Subject: [PATCH 5/7] Only NPCs speak when hit. Farm animal alignment changed
 from NPC to Tame

---
 assets/voxygen/i18n/en.ron       |  2 +-
 common/src/comp/agent.rs         | 22 ++++++++++++++++++++++
 common/src/sys/agent.rs          | 31 ++++++++++++++++++-------------
 server/src/sys/terrain.rs        |  4 +++-
 world/src/site/settlement/mod.rs | 18 +++++++++++++-----
 5 files changed, 57 insertions(+), 20 deletions(-)

diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron
index fef4e49ccb..0cea650ed6 100644
--- a/assets/voxygen/i18n/en.ron
+++ b/assets/voxygen/i18n/en.ron
@@ -402,7 +402,7 @@ Willpower
             "AAAHHH! I'm under attack! Help!",
             "Help! We're under attack!",
             "Help! Murderer!",
-            "Help! There's a murder on the loose!",
+            "Help! There's a murderer on the loose!",
             "Help! They're trying to kill me!",
             "Guards, I'm under attack!",
             "Guards! I'm under attack!",
diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index b0e3ecac60..dffde7484e 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -5,9 +5,15 @@ use vek::*;
 
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum Alignment {
+    /// Wild animals and gentle giants
     Wild,
+    /// Dungeon cultists and bandits
     Enemy,
+    /// Friendly folk in villages
     Npc,
+    /// Farm animals and pets of villagers
+    Tame,
+    /// Pets you've tamed with a collar
     Owned(EcsEntity),
 }
 
@@ -27,6 +33,10 @@ impl Alignment {
         match (self, other) {
             (Alignment::Enemy, Alignment::Enemy) => true,
             (Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
+            (Alignment::Npc, Alignment::Npc) => true,
+            (Alignment::Npc, Alignment::Tame) => true,
+            (Alignment::Tame, Alignment::Npc) => true,
+            (Alignment::Tame, Alignment::Tame) => true,
             _ => false,
         }
     }
@@ -40,6 +50,9 @@ impl Component for Alignment {
 pub struct Agent {
     pub patrol_origin: Option<Vec3<f32>>,
     pub activity: Activity,
+    /// Does the agent talk when e.g. hit by the player
+    // TODO move speech patterns into a Behavior component
+    pub can_speak: bool,
 }
 
 impl Agent {
@@ -47,6 +60,15 @@ impl Agent {
         self.patrol_origin = Some(origin);
         self
     }
+
+    pub fn new(origin: Vec3<f32>, can_speak: bool) -> Self {
+        let patrol_origin = Some(origin);
+        Agent {
+            patrol_origin,
+            can_speak,
+            ..Default::default()
+        }
+    }
 }
 
 impl Component for Agent {
diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs
index 70015af7c8..7dbff74b1c 100644
--- a/common/src/sys/agent.rs
+++ b/common/src/sys/agent.rs
@@ -378,27 +378,32 @@ impl<'a> System<'a> for Sys {
             // last!) ---
 
             // Attack a target that's attacking us
-            if let Some(stats) = stats.get(entity) {
+            if let Some(my_stats) = stats.get(entity) {
                 // Only if the attack was recent
-                if stats.health.last_change.0 < 5.0 {
+                if my_stats.health.last_change.0 < 5.0 {
                     if let comp::HealthSource::Attack { by }
                     | comp::HealthSource::Projectile { owner: Some(by) } =
-                        stats.health.last_change.1.cause
+                        my_stats.health.last_change.1.cause
                     {
                         if !agent.activity.is_attack() {
                             if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
                             {
-                                let message = "npc.speech.villager_under_attack".to_string();
-                                let bubble = SpeechBubble::npc_new(message, *time);
-                                let _ = speech_bubbles.insert(entity, bubble);
+                                if stats.get(attacker).map_or(false, |a| !a.is_dead) {
+                                    if agent.can_speak {
+                                        let message =
+                                            "npc.speech.villager_under_attack".to_string();
+                                        let bubble = SpeechBubble::npc_new(message, *time);
+                                        let _ = speech_bubbles.insert(entity, bubble);
+                                    }
 
-                                agent.activity = Activity::Attack {
-                                    target: attacker,
-                                    chaser: Chaser::default(),
-                                    time: time.0,
-                                    been_close: false,
-                                    powerup: 0.0,
-                                };
+                                    agent.activity = Activity::Attack {
+                                        target: attacker,
+                                        chaser: Chaser::default(),
+                                        time: time.0,
+                                        been_close: false,
+                                        powerup: 0.0,
+                                    };
+                                }
                             }
                         }
                     }
diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs
index 25b838e61a..70268cf7dc 100644
--- a/server/src/sys/terrain.rs
+++ b/server/src/sys/terrain.rs
@@ -329,6 +329,8 @@ impl<'a> System<'a> for Sys {
                     .health
                     .set_to(stats.health.maximum(), comp::HealthSource::Revive);
 
+                let can_speak = alignment == comp::Alignment::Npc;
+
                 // TODO: This code sets an appropriate base_damage for the enemy. This doesn't
                 // work because the damage is now saved in an ability
                 /*
@@ -344,7 +346,7 @@ impl<'a> System<'a> for Sys {
                     loadout,
                     body,
                     alignment,
-                    agent: comp::Agent::default().with_patrol_origin(entity.pos),
+                    agent: comp::Agent::new(entity.pos, can_speak),
                     scale: comp::Scale(scale),
                     drop_item: entity.loot_drop,
                 })
diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs
index c274cff062..23c2793e67 100644
--- a/world/src/site/settlement/mod.rs
+++ b/world/src/site/settlement/mod.rs
@@ -784,8 +784,8 @@ impl Settlement {
                 if matches!(sample.plot, Some(Plot::Town))
                     && RandomField::new(self.seed).chance(Vec3::from(wpos2d), 1.0 / (50.0 * 50.0))
                 {
+                    let is_human: bool;
                     let entity = EntityInfo::at(entity_wpos)
-                        .with_alignment(comp::Alignment::Npc)
                         .with_body(match rng.gen_range(0, 4) {
                             0 => {
                                 let species = match rng.gen_range(0, 3) {
@@ -793,7 +793,7 @@ impl Settlement {
                                     1 => quadruped_small::Species::Sheep,
                                     _ => quadruped_small::Species::Cat,
                                 };
-
+                                is_human = false;
                                 comp::Body::QuadrupedSmall(quadruped_small::Body::random_with(
                                     rng, &species,
                                 ))
@@ -805,14 +805,22 @@ impl Settlement {
                                     2 => bird_medium::Species::Goose,
                                     _ => bird_medium::Species::Peacock,
                                 };
-
+                                is_human = false;
                                 comp::Body::BirdMedium(bird_medium::Body::random_with(
                                     rng, &species,
                                 ))
                             },
-                            _ => comp::Body::Humanoid(humanoid::Body::random()),
+                            _ => {
+                                is_human = true;
+                                comp::Body::Humanoid(humanoid::Body::random())
+                            },
                         })
-                        .do_if(rng.gen(), |entity| {
+                        .with_alignment(if is_human {
+                            comp::Alignment::Npc
+                        } else {
+                            comp::Alignment::Tame
+                        })
+                        .do_if(is_human && rng.gen(), |entity| {
                             entity.with_main_tool(assets::load_expect_cloned(
                                 match rng.gen_range(0, 7) {
                                     0 => "common.items.weapons.tool.broom",

From d5216cc8f34a4428830afcd0b29ee30959eed73d Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Tue, 26 May 2020 12:31:51 -0400
Subject: [PATCH 6/7] Redraw dark mode bubbles. Add padding to light mode
 bubbles.

---
 .../voxygen/element/frames/bubble/bottom.png  |  2 +-
 .../element/frames/bubble/bottom_left.png     |  4 +-
 .../element/frames/bubble/bottom_right.png    |  4 +-
 assets/voxygen/element/frames/bubble/left.png |  2 +-
 .../voxygen/element/frames/bubble/right.png   |  4 +-
 assets/voxygen/element/frames/bubble/tail.png |  2 +-
 assets/voxygen/element/frames/bubble/top.png  |  2 +-
 .../element/frames/bubble/top_left.png        |  4 +-
 .../element/frames/bubble/top_right.png       |  4 +-
 .../element/frames/bubble_dark/bottom.png     |  4 +-
 .../frames/bubble_dark/bottom_left.png        |  4 +-
 .../frames/bubble_dark/bottom_right.png       |  4 +-
 .../element/frames/bubble_dark/left.png       |  4 +-
 .../element/frames/bubble_dark/right.png      |  4 +-
 .../element/frames/bubble_dark/tail.png       |  4 +-
 .../element/frames/bubble_dark/top.png        |  4 +-
 .../element/frames/bubble_dark/top_left.png   |  4 +-
 .../element/frames/bubble_dark/top_right.png  |  4 +-
 voxygen/src/hud/overhead.rs                   | 50 +++++++++----------
 19 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/assets/voxygen/element/frames/bubble/bottom.png b/assets/voxygen/element/frames/bubble/bottom.png
index a133651d2a..248c1269ad 100644
--- a/assets/voxygen/element/frames/bubble/bottom.png
+++ b/assets/voxygen/element/frames/bubble/bottom.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:1b2389dc62c0765c9ed56e53afab639c7fcb90656d83a051e8cf513cda926804
+oid sha256:251f361255513af3996b30c447f884801eeb6ede3539724b72865ba362570616
 size 136
diff --git a/assets/voxygen/element/frames/bubble/bottom_left.png b/assets/voxygen/element/frames/bubble/bottom_left.png
index 514e7f025d..ff26abaf6a 100644
--- a/assets/voxygen/element/frames/bubble/bottom_left.png
+++ b/assets/voxygen/element/frames/bubble/bottom_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:2983324f979fe0ee212e7b4527d4d2ae042ec7da418cee01cdd0637d9410b042
-size 3485
+oid sha256:8957c16fc6d1f1d2ea23a814d1ed4c066b90e8f7bc1c7a9ecd5464c56136d853
+size 199
diff --git a/assets/voxygen/element/frames/bubble/bottom_right.png b/assets/voxygen/element/frames/bubble/bottom_right.png
index 418b586b89..7391d3b157 100644
--- a/assets/voxygen/element/frames/bubble/bottom_right.png
+++ b/assets/voxygen/element/frames/bubble/bottom_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7681e4b43db2d68f8576b26e91c8395f292b9aab76b0fac27ef2f5239e438766
-size 3639
+oid sha256:35a2bef9dc6766ee2614bd7c2434192617b10a321b90a38c40d1dda97d25bbae
+size 214
diff --git a/assets/voxygen/element/frames/bubble/left.png b/assets/voxygen/element/frames/bubble/left.png
index 533725330d..b9f2a14d43 100644
--- a/assets/voxygen/element/frames/bubble/left.png
+++ b/assets/voxygen/element/frames/bubble/left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:6da4e97492532b7c8b83119bd419c5689c00149e217c26dcff6e9628e0eef6de
+oid sha256:15b2b4d70d2d3a2eafb92c4b6aaa30bc2930e609ee2db3c035a02f4c3eaed096
 size 124
diff --git a/assets/voxygen/element/frames/bubble/right.png b/assets/voxygen/element/frames/bubble/right.png
index 210af16f17..c3115ed544 100644
--- a/assets/voxygen/element/frames/bubble/right.png
+++ b/assets/voxygen/element/frames/bubble/right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b17fa4cacc5617de542e8ea054113dba1e8aa19272567f4a8cf026d1e849d0d0
-size 125
+oid sha256:3e137addc48edb5a02697fc71c395e9a9be87af8d88665730a61df172cbe8fdb
+size 124
diff --git a/assets/voxygen/element/frames/bubble/tail.png b/assets/voxygen/element/frames/bubble/tail.png
index 19889b332f..e07bce62fa 100644
--- a/assets/voxygen/element/frames/bubble/tail.png
+++ b/assets/voxygen/element/frames/bubble/tail.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:16f8d6165a5071be33830a33215bbcd5fc4a7783a932bff396df98167e5241bb
+oid sha256:d5113b9561fa79dddbf9b0d98f692d8c7d6e980f3249df03ef9c0052891da11b
 size 227
diff --git a/assets/voxygen/element/frames/bubble/top.png b/assets/voxygen/element/frames/bubble/top.png
index 0c88a60f54..5c94a8ec10 100644
--- a/assets/voxygen/element/frames/bubble/top.png
+++ b/assets/voxygen/element/frames/bubble/top.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:14518edba8bc71829422acd9c3e0c76dc9c78678ed1acc87e81257667eb46a79
+oid sha256:14041a782ce25fe04e2c419f52cbd0e07f0004897ae0f9e85cfd603379e24360
 size 136
diff --git a/assets/voxygen/element/frames/bubble/top_left.png b/assets/voxygen/element/frames/bubble/top_left.png
index e1b770cec8..676927924f 100644
--- a/assets/voxygen/element/frames/bubble/top_left.png
+++ b/assets/voxygen/element/frames/bubble/top_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e4fd9c69f48c863371cef8ed8ea332710b975583b6ba7f0fa91359042a471d32
-size 184
+oid sha256:59a63d20416f94eeb7cf6f3bfe6b9022710a0e2cfe1c669621fd6e375dd13aa5
+size 180
diff --git a/assets/voxygen/element/frames/bubble/top_right.png b/assets/voxygen/element/frames/bubble/top_right.png
index d994a4198c..90d3538cd2 100644
--- a/assets/voxygen/element/frames/bubble/top_right.png
+++ b/assets/voxygen/element/frames/bubble/top_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:8127cb08af479b2e08f3a4decfd5640c97095d7333bf7cd40a4a3e61c8cf09ae
-size 219
+oid sha256:a97673498e8f45266d0e4009ea6237a33471f421b211c8e50c0de69c360abc46
+size 217
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom.png b/assets/voxygen/element/frames/bubble_dark/bottom.png
index 47fc0ae80b..6c29aea33b 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:0649ffdee5c242f0e268de834dab7bacfcd6758cbb40fa30af6ef088b7bb1b88
-size 129
+oid sha256:46628924ae6326359ed3fa0ad2f834cef06a2e387c0dee5a58b167435eab3a37
+size 131
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_left.png b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
index 80e6afd9c3..fcfb10d211 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom_left.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:cbde3c25035fbb219ba954c748fb5ca3f5d57f9d0632ebb4a288238146e95a62
-size 180
+oid sha256:6ca13242a394ccf91cf751d114bb3304aa5ef6ffa28fece1c978769a1de1f2d4
+size 199
diff --git a/assets/voxygen/element/frames/bubble_dark/bottom_right.png b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
index 888a0514ef..6fc710cdd5 100644
--- a/assets/voxygen/element/frames/bubble_dark/bottom_right.png
+++ b/assets/voxygen/element/frames/bubble_dark/bottom_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:aad2eb4c05eb6215c5641478c1ad8f7f5819e524c7fa5f610b1bc2568ebc978a
-size 185
+oid sha256:9fdf825f1f07a7485ffa7492081d5811196367b7a438d757c2401c736447d9d7
+size 220
diff --git a/assets/voxygen/element/frames/bubble_dark/left.png b/assets/voxygen/element/frames/bubble_dark/left.png
index 2a726a2cf3..30c954abbc 100644
--- a/assets/voxygen/element/frames/bubble_dark/left.png
+++ b/assets/voxygen/element/frames/bubble_dark/left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:048b2251d7f2970b8085c8792239f71888aa5344231620af1871ae3cad6fbb1e
-size 126
+oid sha256:524afbf0961dcef3e0798d7e973ed51bb30816c37b1f3353163aff38bd473d23
+size 120
diff --git a/assets/voxygen/element/frames/bubble_dark/right.png b/assets/voxygen/element/frames/bubble_dark/right.png
index f5682b6f24..de076ca355 100644
--- a/assets/voxygen/element/frames/bubble_dark/right.png
+++ b/assets/voxygen/element/frames/bubble_dark/right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:35af73b41aa32b94276f143e3156bdca0151590162a610f7700f898011bdddfa
-size 131
+oid sha256:d1caf5ee920d29eb2f690bdcb0fad4cc2f029bbfce464af0fccf01540607d33c
+size 120
diff --git a/assets/voxygen/element/frames/bubble_dark/tail.png b/assets/voxygen/element/frames/bubble_dark/tail.png
index 3cc41634fa..32cb0a0222 100644
--- a/assets/voxygen/element/frames/bubble_dark/tail.png
+++ b/assets/voxygen/element/frames/bubble_dark/tail.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:0b21fefeab508b57af56eaa72153cd584fa8f6db41f639778368b46d2a0f2f38
-size 221
+oid sha256:e1d6707a666716bf2a935705cb015b86e1ee91e0cd0cbc607be99caa598f2cdc
+size 247
diff --git a/assets/voxygen/element/frames/bubble_dark/top.png b/assets/voxygen/element/frames/bubble_dark/top.png
index 01d893ab00..77651db5fb 100644
--- a/assets/voxygen/element/frames/bubble_dark/top.png
+++ b/assets/voxygen/element/frames/bubble_dark/top.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:59fba25aad27f065ee5e2446400a1d9ce1a98b59b100ab9328880d1c25f9031a
-size 129
+oid sha256:6f84fb7cd1ff7df43d944d52598d320abd7e7d24182e9c969293b62818755c36
+size 132
diff --git a/assets/voxygen/element/frames/bubble_dark/top_left.png b/assets/voxygen/element/frames/bubble_dark/top_left.png
index c89832ec6f..b6d6f6c0b3 100644
--- a/assets/voxygen/element/frames/bubble_dark/top_left.png
+++ b/assets/voxygen/element/frames/bubble_dark/top_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:33c47f93a1641466781dea3fb5115ccb689ed80a210875032d7cddce481fce47
-size 169
+oid sha256:d8470ac055477bc559fc8199556d1d880586c5f865a2127d619fefff35f3f937
+size 177
diff --git a/assets/voxygen/element/frames/bubble_dark/top_right.png b/assets/voxygen/element/frames/bubble_dark/top_right.png
index 14aeb65943..38bc5f9192 100644
--- a/assets/voxygen/element/frames/bubble_dark/top_right.png
+++ b/assets/voxygen/element/frames/bubble_dark/top_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:60c4e98e21e2e738552833a080e80ab02bc61a5bbec99101d7c9b75c39caeb7c
-size 186
+oid sha256:fea204668ba0bd8ec95023ba642347ea9832a2b172b469854b8582359783586a
+size 207
diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs
index bfe14afd76..238f48a3d2 100644
--- a/voxygen/src/hud/overhead.rs
+++ b/voxygen/src/hud/overhead.rs
@@ -150,7 +150,7 @@ impl<'a> Widget for Overhead<'a> {
             let mut text = Text::new(&bubble_contents)
                 .font_id(self.fonts.cyri.conrod_id)
                 .font_size(18)
-                .up_from(state.ids.name, 10.0)
+                .up_from(state.ids.name, 20.0)
                 .x_align_to(state.ids.name, Align::Middle)
                 .parent(id);
             text = if dark_mode {
@@ -168,8 +168,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_top_left
             })
-            .w_h(10.0, 10.0)
-            .top_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w_h(16.0, 16.0)
+            .top_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_top_left, ui);
             Image::new(if dark_mode {
@@ -177,9 +177,9 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_top
             })
-            .h(10.0)
-            .w_of(state.ids.speech_bubble_text)
-            .mid_top_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .h(16.0)
+            .padded_w_of(state.ids.speech_bubble_text, -4.0)
+            .mid_top_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_top, ui);
             Image::new(if dark_mode {
@@ -187,8 +187,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_top_right
             })
-            .w_h(10.0, 10.0)
-            .top_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w_h(16.0, 16.0)
+            .top_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_top_right, ui);
             Image::new(if dark_mode {
@@ -196,9 +196,9 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_left
             })
-            .w(10.0)
-            .h_of(state.ids.speech_bubble_text)
-            .mid_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w(16.0)
+            .padded_h_of(state.ids.speech_bubble_text, -4.0)
+            .mid_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_left, ui);
             Image::new(if dark_mode {
@@ -206,8 +206,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_mid
             })
-            .wh_of(state.ids.speech_bubble_text)
-            .top_left_of(state.ids.speech_bubble_text)
+            .padded_wh_of(state.ids.speech_bubble_text, -4.0)
+            .top_left_with_margin_on(state.ids.speech_bubble_text, -4.0)
             .parent(id)
             .set(state.ids.speech_bubble_mid, ui);
             Image::new(if dark_mode {
@@ -215,9 +215,9 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_right
             })
-            .w(10.0)
-            .h_of(state.ids.speech_bubble_text)
-            .mid_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w(16.0)
+            .padded_h_of(state.ids.speech_bubble_text, -4.0)
+            .mid_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_right, ui);
             Image::new(if dark_mode {
@@ -225,8 +225,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_bottom_left
             })
-            .w_h(10.0, 10.0)
-            .bottom_left_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w_h(16.0, 16.0)
+            .bottom_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_bottom_left, ui);
             Image::new(if dark_mode {
@@ -234,9 +234,9 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_bottom
             })
-            .h(10.0)
-            .w_of(state.ids.speech_bubble_text)
-            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .h(16.0)
+            .padded_w_of(state.ids.speech_bubble_text, -4.0)
+            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_bottom, ui);
             Image::new(if dark_mode {
@@ -244,8 +244,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_bottom_right
             })
-            .w_h(10.0, 10.0)
-            .bottom_right_with_margin_on(state.ids.speech_bubble_text, -10.0)
+            .w_h(16.0, 16.0)
+            .bottom_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
             .parent(id)
             .set(state.ids.speech_bubble_bottom_right, ui);
             let tail = Image::new(if dark_mode {
@@ -253,8 +253,8 @@ impl<'a> Widget for Overhead<'a> {
             } else {
                 self.imgs.speech_bubble_tail
             })
-            .w_h(11.0, 16.0)
-            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -16.0)
+            .w_h(22.0, 28.0)
+            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0)
             .parent(id);
             // Move text to front (conrod depth is lowest first; not a z-index)
             tail.set(state.ids.speech_bubble_tail, ui);

From 72e90d53767d21ff2284c488fc49426d24fb78aa Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Tue, 26 May 2020 12:55:13 -0400
Subject: [PATCH 7/7] speech bubble changelog

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index de9b21f47f..881cbdb84f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added context-sensitive crosshair
 - Announce alias changes to all clients.
 - Dance animation
+- Speech bubbles appear when nearby players talk
+- NPCs call for help when attacked
 
 ### Changed