veloren_voxygen/hud/
map.rs

1use super::{
2    MapMarkers, QUALITY_COMMON, QUALITY_EPIC, QUALITY_HIGH, QUALITY_LOW, QUALITY_MODERATE, TEXT_BG,
3    TEXT_BLUE_COLOR, TEXT_COLOR, TEXT_GRAY_COLOR, TEXT_VELORITE, UI_HIGHLIGHT_0, UI_MAIN,
4    img_ids::{Imgs, ImgsRot},
5};
6use crate::{
7    GlobalState,
8    game_input::GameInput,
9    session::settings_change::{Interface as InterfaceChange, Interface::*},
10    ui::{ImageFrame, Tooltip, TooltipManager, Tooltipable, fonts::Fonts, img_ids},
11    window::KeyMouse,
12};
13use client::{self, Client, SiteMarker};
14use common::{
15    comp,
16    comp::group::Role,
17    map::{Marker, MarkerFlags, MarkerKind},
18    terrain::{CoordinateConversions, TerrainChunkSize},
19    trade::Good,
20    vol::RectVolSize,
21};
22use common_net::msg::world_msg::{PoiKind, SiteId};
23use conrod_core::{
24    Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
25    input::MouseButton as ConrodMouseButton,
26    position,
27    widget::{self, Button, Image, Rectangle, Text},
28    widget_ids,
29};
30use i18n::Localization;
31use specs::WorldExt;
32use std::borrow::Cow;
33use vek::*;
34use winit::event::MouseButton;
35
36widget_ids! {
37    struct Ids {
38        frame,
39        bg,
40        icon,
41        close,
42        title,
43        map_align,
44        qlog_align,
45        location_name,
46        indicator,
47        indicator_overlay,
48        map_layers[],
49        map_title,
50        qlog_title,
51        zoom_slider,
52        mmap_site_icons[],
53        mmap_poi_icons[],
54        mmap_poi_title_bgs[],
55        mmap_poi_titles[],
56        peaks_txt,
57        peaks_txt_bg,
58        site_difs[],
59        member_indicators[],
60        member_height_indicators[],
61        location_marker,
62        location_marker_group[],
63        map_settings_align,
64        show_towns_img,
65        show_towns_box,
66        show_towns_text,
67        show_sea_chapels_img,
68        show_sea_chapels_box,
69        show_sea_chapels_text,
70        show_castles_img,
71        show_castles_box,
72        show_castles_text,
73        show_bridges_img,
74        show_bridges_box,
75        show_bridges_text,
76        show_dungeons_img,
77        show_dungeons_box,
78        show_dungeons_text,
79        show_caves_img,
80        show_caves_box,
81        show_caves_text,
82        show_trees_img,
83        show_trees_box,
84        show_trees_text,
85        show_peaks_img,
86        show_peaks_box,
87        show_peaks_text,
88        show_biomes_img,
89        show_biomes_box,
90        show_biomes_text,
91        show_glider_courses_img,
92        show_glider_courses_box,
93        show_glider_courses_text,
94        show_quests_img,
95        show_quests_box,
96        show_quests_text,
97        show_voxel_map_img,
98        show_voxel_map_box,
99        show_voxel_map_text,
100        show_difficulty_img,
101        show_difficulty_box,
102        show_difficulty_text,
103        recenter_button,
104        drag_txt,
105        drag_ico,
106        zoom_txt,
107        zoom_ico,
108        waypoint_binding_txt,
109        waypoint_txt,
110        map_mode_btn,
111        map_mode_overlay,
112        minimap_mode_btn,
113        minimap_mode_overlay,
114
115    }
116}
117
118const SHOW_ECONOMY: bool = false; // turn this display off (for 0.9) until we have an improved look
119
120pub struct ExtraMarker {
121    pub recv_pos: Vec2<f32>,
122    pub marker: Marker,
123}
124
125#[derive(WidgetCommon)]
126pub struct Map<'a> {
127    client: &'a Client,
128    world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
129    imgs: &'a Imgs,
130    fonts: &'a Fonts,
131    #[conrod(common_builder)]
132    common: widget::CommonBuilder,
133    pulse: f32,
134    localized_strings: &'a Localization,
135    global_state: &'a GlobalState,
136    rot_imgs: &'a ImgsRot,
137    tooltip_manager: &'a mut TooltipManager,
138    location_markers: &'a MapMarkers,
139    map_drag: Vec2<f64>,
140    extra_markers: &'a [ExtraMarker],
141}
142impl<'a> Map<'a> {
143    pub fn new(
144        client: &'a Client,
145        imgs: &'a Imgs,
146        rot_imgs: &'a ImgsRot,
147        world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
148        fonts: &'a Fonts,
149        pulse: f32,
150        localized_strings: &'a Localization,
151        global_state: &'a GlobalState,
152        tooltip_manager: &'a mut TooltipManager,
153        location_markers: &'a MapMarkers,
154        map_drag: Vec2<f64>,
155        extra_markers: &'a [ExtraMarker],
156    ) -> Self {
157        Self {
158            imgs,
159            rot_imgs,
160            world_map,
161            client,
162            fonts,
163            common: widget::CommonBuilder::default(),
164            pulse,
165            localized_strings,
166            global_state,
167            tooltip_manager,
168            location_markers,
169            map_drag,
170            extra_markers,
171        }
172    }
173}
174
175pub struct State {
176    ids: Ids,
177}
178
179pub enum Event {
180    SettingsChange(InterfaceChange),
181    Close,
182    RequestSiteInfo(SiteId),
183    SetLocationMarker(Vec2<i32>),
184    MapDrag(Vec2<f64>),
185    RemoveMarker,
186}
187
188fn get_site_economy(site: &SiteMarker) -> String {
189    if SHOW_ECONOMY {
190        if let Some(economy) = &site.economy {
191            use common::trade::Good::{Armor, Coin, Food, Ingredients, Potions, Tools};
192            let mut result = format!("\n\nPopulation {:?}", economy.population);
193            result += "\nStock";
194            for i in [Food, Potions, Ingredients, Coin, Tools, Armor].iter() {
195                result += &format!("\n  {:?}={:.3}", *i, *economy.stock.get(i).unwrap_or(&0.0));
196            }
197            result += "\nPrice";
198            for i in [Food, Potions, Ingredients, Coin, Tools, Armor].iter() {
199                result += &format!("\n  {:?}={:.3}", *i, *economy.values.get(i).unwrap_or(&0.0));
200            }
201
202            let mut trade_sorted: Vec<(&Good, &f32)> = economy.last_exports.iter().collect();
203            trade_sorted.sort_unstable_by(|a, b| a.1.partial_cmp(b.1).unwrap());
204            if !trade_sorted.is_empty() {
205                result += &format!("\nTrade {:.1} ", *(trade_sorted.first().unwrap().1));
206                for i in trade_sorted.iter().filter(|x| *x.1 != 0.0) {
207                    result += &format!("{:?} ", i.0);
208                }
209                result += &format!("{:.3}", *(trade_sorted.last().unwrap().1));
210            }
211            result
212        } else {
213            format!("\nloading economy for\n{:?}", site.marker.site)
214        }
215    } else {
216        "".into()
217    }
218}
219
220impl From<&KeyMouse> for ConrodMouseButton {
221    fn from(key: &KeyMouse) -> Self {
222        match key {
223            KeyMouse::Mouse(MouseButton::Left) => ConrodMouseButton::Left,
224            KeyMouse::Mouse(MouseButton::Right) => ConrodMouseButton::Right,
225            KeyMouse::Mouse(MouseButton::Middle) => ConrodMouseButton::Middle,
226            KeyMouse::Mouse(MouseButton::Other(0)) => ConrodMouseButton::X1,
227            KeyMouse::Mouse(MouseButton::Other(1)) => ConrodMouseButton::X2,
228            KeyMouse::Mouse(MouseButton::Other(2)) => ConrodMouseButton::Button6,
229            KeyMouse::Mouse(MouseButton::Other(3)) => ConrodMouseButton::Button7,
230            KeyMouse::Mouse(MouseButton::Other(4)) => ConrodMouseButton::Button8,
231            _ => conrod_core::input::MouseButton::Unknown,
232        }
233    }
234}
235
236impl Widget for Map<'_> {
237    type Event = Vec<Event>;
238    type State = State;
239    type Style = ();
240
241    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
242        State {
243            ids: Ids::new(id_gen),
244        }
245    }
246
247    fn style(&self) -> Self::Style {}
248
249    #[expect(clippy::neg_multiply)]
250    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
251        common_base::prof_span!("Map::update");
252        let widget::UpdateArgs { state, ui, .. } = args;
253        let colored_player_marker = self
254            .global_state
255            .settings
256            .interface
257            .minimap_colored_player_marker;
258        let zoom = self.global_state.settings.interface.map_zoom;
259        let show_difficulty = self.global_state.settings.interface.map_show_difficulty;
260        let show_towns = self.global_state.settings.interface.map_show_towns;
261        let show_dungeons = self.global_state.settings.interface.map_show_dungeons;
262        let show_castles = self.global_state.settings.interface.map_show_castles;
263        let show_bridges = self.global_state.settings.interface.map_show_bridges;
264        let show_caves = self.global_state.settings.interface.map_show_caves;
265        let show_trees = self.global_state.settings.interface.map_show_trees;
266        let show_peaks = self.global_state.settings.interface.map_show_peaks;
267        let show_biomes = self.global_state.settings.interface.map_show_biomes;
268        let show_glider_courses = self.global_state.settings.interface.map_show_glider_courses;
269        let show_quests = self.global_state.settings.interface.map_show_quests;
270        let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
271        let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
272        let location_marker_binding = self
273            .global_state
274            .settings
275            .controls
276            .keybindings
277            .get(&GameInput::MapSetMarker)
278            .cloned()
279            .flatten()
280            .unwrap_or(KeyMouse::Mouse(MouseButton::Middle));
281        let mut events = Vec::new();
282        let i18n = &self.localized_strings;
283        // Tooltips
284        let site_tooltip = Tooltip::new({
285            // Edge images [t, b, r, l]
286            // Corner images [tr, tl, br, bl]
287            let edge = &self.rot_imgs.tt_side;
288            let corner = &self.rot_imgs.tt_corner;
289            ImageFrame::new(
290                [edge.cw180, edge.none, edge.cw270, edge.cw90],
291                [corner.none, corner.cw270, corner.cw90, corner.cw180],
292                Color::Rgba(0.08, 0.07, 0.04, 1.0),
293                5.0,
294            )
295        })
296        .title_font_size(self.fonts.cyri.scale(15))
297        .parent(ui.window)
298        .desc_font_size(self.fonts.cyri.scale(12))
299        .font_id(self.fonts.cyri.conrod_id)
300        .desc_text_color(TEXT_COLOR);
301        // Frame
302        Image::new(self.imgs.map_bg)
303            .w_h(1202.0, 886.0)
304            .mid_top_with_margin_on(ui.window, 5.0)
305            .color(Some(UI_MAIN))
306            .set(state.ids.bg, ui);
307
308        Image::new(self.imgs.map_frame)
309            .w_h(1202.0, 886.0)
310            .middle_of(state.ids.bg)
311            .color(Some(UI_HIGHLIGHT_0))
312            .set(state.ids.frame, ui);
313
314        // Map Content Alignment
315        Rectangle::fill_with([814.0, 834.0], color::TRANSPARENT)
316            .top_left_with_margins_on(state.ids.frame, 46.0, 240.0)
317            .set(state.ids.map_align, ui);
318
319        // Questlog Content Alignment
320        Rectangle::fill_with([232.0, 814.0], color::TRANSPARENT)
321            .top_left_with_margins_on(state.ids.frame, 44.0, 2.0)
322            .set(state.ids.qlog_align, ui);
323
324        // Icon
325        Image::new(self.imgs.map_icon)
326            .w_h(30.0, 30.0)
327            .top_left_with_margins_on(state.ids.frame, 6.0, 8.0)
328            .set(state.ids.icon, ui);
329
330        // Map Title
331        Text::new(&i18n.get_msg("hud-map-map_title"))
332            .mid_top_with_margin_on(state.ids.frame, 3.0)
333            .font_id(self.fonts.cyri.conrod_id)
334            .font_size(self.fonts.cyri.scale(29))
335            .color(TEXT_COLOR)
336            .set(state.ids.map_title, ui);
337
338        // Questlog Title
339        Text::new(&i18n.get_msg("hud-map-qlog_title"))
340            .mid_top_with_margin_on(state.ids.qlog_align, 6.0)
341            .font_id(self.fonts.cyri.conrod_id)
342            .font_size(self.fonts.cyri.scale(21))
343            .color(TEXT_COLOR)
344            .set(state.ids.qlog_title, ui);
345
346        // Location Name
347        /*match self.client.current_chunk() {
348            Some(chunk) => Text::new(chunk.meta().name())
349                .mid_top_with_margin_on(state.ids.bg, 55.0)
350                .font_size(self.fonts.alkhemi.scale(60))
351                .color(TEXT_COLOR)
352                .font_id(self.fonts.alkhemi.conrod_id)
353                .parent(state.ids.frame)
354                .set(state.ids.location_name, ui),
355            None => Text::new(" ")
356                .mid_top_with_margin_on(state.ids.bg, 3.0)
357                .font_size(self.fonts.alkhemi.scale(40))
358                .font_id(self.fonts.alkhemi.conrod_id)
359                .color(TEXT_COLOR)
360                .set(state.ids.location_name, ui),
361        }*/
362        // Map Layers
363        // It is assumed that there is at least one layer
364        if state.ids.map_layers.len() < self.world_map.0.len() {
365            state.update(|state| {
366                state
367                    .ids
368                    .map_layers
369                    .resize(self.world_map.0.len(), &mut ui.widget_id_generator())
370            });
371        }
372
373        // Map Size
374        let worldsize = self.world_map.1;
375
376        // Coordinates
377        let player_pos = self
378            .client
379            .state()
380            .ecs()
381            .read_storage::<comp::Pos>()
382            .get(self.client.entity())
383            .map_or(Vec3::zero(), |pos| pos.0);
384
385        let map_size = Vec2::new(760.0, 760.0);
386
387        let player_pos_chunks =
388            player_pos.xy().map(|x| x as f64) / TerrainChunkSize::RECT_SIZE.map(|x| x as f64);
389        let min_drag = player_pos_chunks - worldsize.map(|x| x as f64);
390        let max_drag = player_pos_chunks;
391        let drag = self.map_drag.clamped(min_drag, max_drag);
392
393        enum MarkerChange {
394            Pos(Vec2<f32>),
395            ClickPos,
396            Remove,
397        }
398
399        let handle_widget_mouse_events =
400            |widget, marker: MarkerChange, ui: &mut UiCell, events: &mut Vec<Event>, map_widget| {
401                // Handle Location Marking
402                if let Some(click) = ui
403                    .widget_input(widget)
404                    .clicks()
405                    .button(ConrodMouseButton::from(&location_marker_binding))
406                    .next()
407                {
408                    match marker {
409                        MarkerChange::Pos(ref wpos) => {
410                            events.push(Event::SetLocationMarker(wpos.as_()))
411                        },
412                        MarkerChange::ClickPos => {
413                            let tmp: Vec2<f64> = Vec2::<f64>::from(click.xy) / zoom - drag;
414                            let wpos = tmp.as_::<f32>().cpos_to_wpos() + player_pos;
415                            events.push(Event::SetLocationMarker(wpos.as_()));
416                        },
417                        MarkerChange::Remove => events.push(Event::RemoveMarker),
418                    }
419                }
420
421                // Handle zooming with the mouse wheel
422                let scrolled: f64 = ui
423                    .widget_input(widget)
424                    .scrolls()
425                    .map(|scroll| scroll.y)
426                    .sum();
427                if scrolled != 0.0 {
428                    let max_zoom = 16.0;
429                    let min_zoom = map_size.x / worldsize.reduce_partial_max() as f64 / 2.0;
430                    let new_zoom_lvl: f64 = (f64::log2(zoom) - scrolled * 0.03)
431                        .exp2()
432                        .clamp(min_zoom.min(max_zoom), max_zoom); // min_zoom can > 16 for small maps
433                    events.push(Event::SettingsChange(MapZoom(new_zoom_lvl)));
434                    let cursor_mouse_pos = ui
435                        .widget_input(map_widget)
436                        .mouse()
437                        .map(|mouse| mouse.rel_xy());
438                    if let Some(cursor_pos) = cursor_mouse_pos {
439                        let mouse_pos = Vec2::from_slice(&cursor_pos);
440                        let drag_new = drag + mouse_pos * (1.0 / new_zoom_lvl - 1.0 / zoom);
441                        if drag_new != drag {
442                            events.push(Event::MapDrag(drag_new));
443                        }
444                    }
445                }
446
447                // Handle dragging
448                let dragged: Vec2<f64> = ui
449                    .widget_input(widget)
450                    .drags()
451                    .left()
452                    .map(|drag| Vec2::<f64>::from(drag.delta_xy))
453                    .sum();
454                // Drag represents offset of view from the player_pos in chunk coords
455                let drag_new = drag + dragged / zoom;
456                if drag_new != drag {
457                    events.push(Event::MapDrag(drag_new));
458                }
459            };
460
461        handle_widget_mouse_events(
462            state.ids.map_layers[0],
463            MarkerChange::ClickPos,
464            ui,
465            &mut events,
466            state.ids.map_layers[0],
467        );
468
469        let rect_src = position::Rect::from_xy_dim(
470            [
471                (player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64) - drag.x,
472                (worldsize.y as f64 - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64))
473                    + drag.y,
474            ],
475            [map_size.x / zoom, map_size.y / zoom],
476        );
477
478        // X-Button
479        if Button::image(self.imgs.close_button)
480            .w_h(24.0, 25.0)
481            .hover_image(self.imgs.close_btn_hover)
482            .press_image(self.imgs.close_btn_press)
483            .top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
484            .set(state.ids.close, ui)
485            .was_clicked()
486        {
487            events.push(Event::Close);
488        }
489
490        // Map Layer Images
491        for (index, layer) in self.world_map.0.iter().enumerate() {
492            if index == 0 {
493                Button::image(layer.none)
494                    .mid_top_with_margin_on(state.ids.map_align, 10.0)
495                    .w_h(map_size.x, map_size.y)
496                    .parent(state.ids.bg)
497                    .source_rectangle(rect_src)
498                    .set(state.ids.map_layers[index], ui);
499            } else if show_topo_map {
500                Button::image(layer.none)
501                    .mid_top_with_margin_on(state.ids.map_align, 10.0)
502                    .w_h(map_size.x, map_size.y)
503                    .parent(state.ids.bg)
504                    .source_rectangle(rect_src)
505                    .graphics_for(state.ids.map_layers[0])
506                    .set(state.ids.map_layers[index], ui);
507            }
508        }
509
510        // Icon settings
511        // Alignment
512        Rectangle::fill_with([150.0, 200.0], color::TRANSPARENT)
513            .top_right_with_margins_on(state.ids.frame, 55.0, 10.0)
514            .set(state.ids.map_settings_align, ui);
515        // Checkboxes
516        // Show difficulties
517        Image::new(self.imgs.map_dif_icon)
518            .top_left_with_margins_on(state.ids.map_settings_align, 5.0, 5.0)
519            .w_h(20.0, 20.0)
520            .set(state.ids.show_difficulty_img, ui);
521        if Button::image(if show_difficulty {
522            self.imgs.checkbox_checked
523        } else {
524            self.imgs.checkbox
525        })
526        .w_h(18.0, 18.0)
527        .hover_image(if show_difficulty {
528            self.imgs.checkbox_checked_mo
529        } else {
530            self.imgs.checkbox_mo
531        })
532        .press_image(if show_difficulty {
533            self.imgs.checkbox_checked
534        } else {
535            self.imgs.checkbox_press
536        })
537        .right_from(state.ids.show_difficulty_img, 10.0)
538        .set(state.ids.show_difficulty_box, ui)
539        .was_clicked()
540        {
541            events.push(Event::SettingsChange(MapShowDifficulty(!show_difficulty)));
542        }
543        Text::new(&i18n.get_msg("hud-map-difficulty"))
544            .right_from(state.ids.show_difficulty_box, 10.0)
545            .font_size(self.fonts.cyri.scale(14))
546            .font_id(self.fonts.cyri.conrod_id)
547            .graphics_for(state.ids.show_difficulty_box)
548            .color(TEXT_COLOR)
549            .set(state.ids.show_difficulty_text, ui);
550        // Towns
551        Image::new(self.imgs.mmap_site_town)
552            .down_from(state.ids.show_difficulty_img, 10.0)
553            .w_h(20.0, 20.0)
554            .set(state.ids.show_towns_img, ui);
555        if Button::image(if show_towns {
556            self.imgs.checkbox_checked
557        } else {
558            self.imgs.checkbox
559        })
560        .w_h(18.0, 18.0)
561        .hover_image(if show_towns {
562            self.imgs.checkbox_checked_mo
563        } else {
564            self.imgs.checkbox_mo
565        })
566        .press_image(if show_towns {
567            self.imgs.checkbox_checked
568        } else {
569            self.imgs.checkbox_press
570        })
571        .right_from(state.ids.show_towns_img, 10.0)
572        .set(state.ids.show_towns_box, ui)
573        .was_clicked()
574        {
575            events.push(Event::SettingsChange(MapShowTowns(!show_towns)));
576        }
577        Text::new(&i18n.get_msg("hud-map-towns"))
578            .right_from(state.ids.show_towns_box, 10.0)
579            .font_size(self.fonts.cyri.scale(14))
580            .font_id(self.fonts.cyri.conrod_id)
581            .graphics_for(state.ids.show_towns_box)
582            .color(TEXT_COLOR)
583            .set(state.ids.show_towns_text, ui);
584        // Castles
585        Image::new(self.imgs.mmap_site_castle)
586            .down_from(state.ids.show_towns_img, 10.0)
587            .w_h(20.0, 20.0)
588            .set(state.ids.show_castles_img, ui);
589        if Button::image(if show_castles {
590            self.imgs.checkbox_checked
591        } else {
592            self.imgs.checkbox
593        })
594        .w_h(18.0, 18.0)
595        .hover_image(if show_castles {
596            self.imgs.checkbox_checked_mo
597        } else {
598            self.imgs.checkbox_mo
599        })
600        .press_image(if show_castles {
601            self.imgs.checkbox_checked
602        } else {
603            self.imgs.checkbox_press
604        })
605        .right_from(state.ids.show_castles_img, 10.0)
606        .set(state.ids.show_castles_box, ui)
607        .was_clicked()
608        {
609            events.push(Event::SettingsChange(MapShowCastles(!show_castles)));
610        }
611        Text::new(&i18n.get_msg("hud-map-castles"))
612            .right_from(state.ids.show_castles_box, 10.0)
613            .font_size(self.fonts.cyri.scale(14))
614            .font_id(self.fonts.cyri.conrod_id)
615            .graphics_for(state.ids.show_castles_box)
616            .color(TEXT_COLOR)
617            .set(state.ids.show_castles_text, ui);
618        // Bridges
619        Image::new(self.imgs.mmap_site_bridge)
620            .down_from(state.ids.show_castles_img, 10.0)
621            .w_h(20.0, 20.0)
622            .set(state.ids.show_bridges_img, ui);
623        if Button::image(if show_bridges {
624            self.imgs.checkbox_checked
625        } else {
626            self.imgs.checkbox
627        })
628        .w_h(18.0, 18.0)
629        .hover_image(if show_bridges {
630            self.imgs.checkbox_checked_mo
631        } else {
632            self.imgs.checkbox_mo
633        })
634        .press_image(if show_bridges {
635            self.imgs.checkbox_checked
636        } else {
637            self.imgs.checkbox_press
638        })
639        .right_from(state.ids.show_bridges_img, 10.0)
640        .set(state.ids.show_bridges_box, ui)
641        .was_clicked()
642        {
643            events.push(Event::SettingsChange(MapShowBridges(!show_bridges)));
644        }
645        Text::new(&i18n.get_msg("hud-map-bridges"))
646            .right_from(state.ids.show_bridges_box, 10.0)
647            .font_size(self.fonts.cyri.scale(14))
648            .font_id(self.fonts.cyri.conrod_id)
649            .graphics_for(state.ids.show_bridges_box)
650            .color(TEXT_COLOR)
651            .set(state.ids.show_bridges_text, ui);
652        // Dungeons
653        Image::new(self.imgs.mmap_site_dungeon)
654            .down_from(state.ids.show_bridges_img, 10.0)
655            .w_h(20.0, 20.0)
656            .set(state.ids.show_dungeons_img, ui);
657        if Button::image(if show_dungeons {
658            self.imgs.checkbox_checked
659        } else {
660            self.imgs.checkbox
661        })
662        .w_h(18.0, 18.0)
663        .hover_image(if show_dungeons {
664            self.imgs.checkbox_checked_mo
665        } else {
666            self.imgs.checkbox_mo
667        })
668        .press_image(if show_dungeons {
669            self.imgs.checkbox_checked
670        } else {
671            self.imgs.checkbox_press
672        })
673        .right_from(state.ids.show_dungeons_img, 10.0)
674        .set(state.ids.show_dungeons_box, ui)
675        .was_clicked()
676        {
677            events.push(Event::SettingsChange(MapShowDungeons(!show_dungeons)));
678        }
679        Text::new(&i18n.get_msg("hud-map-dungeons"))
680            .right_from(state.ids.show_dungeons_box, 10.0)
681            .font_size(self.fonts.cyri.scale(14))
682            .font_id(self.fonts.cyri.conrod_id)
683            .graphics_for(state.ids.show_dungeons_box)
684            .color(TEXT_COLOR)
685            .set(state.ids.show_dungeons_text, ui);
686        // Caves
687        Image::new(self.imgs.mmap_site_cave)
688            .down_from(state.ids.show_dungeons_img, 10.0)
689            .w_h(20.0, 20.0)
690            .set(state.ids.show_caves_img, ui);
691        if Button::image(if show_caves {
692            self.imgs.checkbox_checked
693        } else {
694            self.imgs.checkbox
695        })
696        .w_h(18.0, 18.0)
697        .hover_image(if show_caves {
698            self.imgs.checkbox_checked_mo
699        } else {
700            self.imgs.checkbox_mo
701        })
702        .press_image(if show_caves {
703            self.imgs.checkbox_checked
704        } else {
705            self.imgs.checkbox_press
706        })
707        .right_from(state.ids.show_caves_img, 10.0)
708        .set(state.ids.show_caves_box, ui)
709        .was_clicked()
710        {
711            events.push(Event::SettingsChange(MapShowCaves(!show_caves)));
712        }
713        Text::new(&i18n.get_msg("hud-map-caves"))
714            .right_from(state.ids.show_caves_box, 10.0)
715            .font_size(self.fonts.cyri.scale(14))
716            .font_id(self.fonts.cyri.conrod_id)
717            .graphics_for(state.ids.show_caves_box)
718            .color(TEXT_COLOR)
719            .set(state.ids.show_caves_text, ui);
720        // Trees
721        Image::new(self.imgs.mmap_site_tree)
722            .down_from(state.ids.show_caves_img, 10.0)
723            .w_h(20.0, 20.0)
724            .set(state.ids.show_trees_img, ui);
725        if Button::image(if show_trees {
726            self.imgs.checkbox_checked
727        } else {
728            self.imgs.checkbox
729        })
730        .w_h(18.0, 18.0)
731        .hover_image(if show_trees {
732            self.imgs.checkbox_checked_mo
733        } else {
734            self.imgs.checkbox_mo
735        })
736        .press_image(if show_trees {
737            self.imgs.checkbox_checked
738        } else {
739            self.imgs.checkbox_press
740        })
741        .right_from(state.ids.show_trees_img, 10.0)
742        .set(state.ids.show_trees_box, ui)
743        .was_clicked()
744        {
745            events.push(Event::SettingsChange(MapShowTrees(!show_trees)));
746        }
747        Text::new(&i18n.get_msg("hud-map-trees"))
748            .right_from(state.ids.show_trees_box, 10.0)
749            .font_size(self.fonts.cyri.scale(14))
750            .font_id(self.fonts.cyri.conrod_id)
751            .graphics_for(state.ids.show_trees_box)
752            .color(TEXT_COLOR)
753            .set(state.ids.show_trees_text, ui);
754        // Biomes
755        Image::new(self.imgs.mmap_poi_biome)
756            .down_from(state.ids.show_trees_img, 10.0)
757            .w_h(20.0, 20.0)
758            .set(state.ids.show_biomes_img, ui);
759        if Button::image(if show_biomes {
760            self.imgs.checkbox_checked
761        } else {
762            self.imgs.checkbox
763        })
764        .w_h(18.0, 18.0)
765        .hover_image(if show_biomes {
766            self.imgs.checkbox_checked_mo
767        } else {
768            self.imgs.checkbox_mo
769        })
770        .press_image(if show_biomes {
771            self.imgs.checkbox_checked
772        } else {
773            self.imgs.checkbox_press
774        })
775        .right_from(state.ids.show_biomes_img, 10.0)
776        .set(state.ids.show_biomes_box, ui)
777        .was_clicked()
778        {
779            events.push(Event::SettingsChange(MapShowBiomes(!show_biomes)));
780        }
781        Text::new(&i18n.get_msg("hud-map-biomes"))
782            .right_from(state.ids.show_biomes_box, 10.0)
783            .font_size(self.fonts.cyri.scale(14))
784            .font_id(self.fonts.cyri.conrod_id)
785            .graphics_for(state.ids.show_biomes_box)
786            .color(TEXT_COLOR)
787            .set(state.ids.show_biomes_text, ui);
788        // Peaks
789        Image::new(self.imgs.mmap_poi_peak)
790            .down_from(state.ids.show_biomes_img, 10.0)
791            .w_h(20.0, 20.0)
792            .set(state.ids.show_peaks_img, ui);
793        if Button::image(if show_peaks {
794            self.imgs.checkbox_checked
795        } else {
796            self.imgs.checkbox
797        })
798        .w_h(18.0, 18.0)
799        .hover_image(if show_peaks {
800            self.imgs.checkbox_checked_mo
801        } else {
802            self.imgs.checkbox_mo
803        })
804        .press_image(if show_peaks {
805            self.imgs.checkbox_checked
806        } else {
807            self.imgs.checkbox_press
808        })
809        .right_from(state.ids.show_peaks_img, 10.0)
810        .set(state.ids.show_peaks_box, ui)
811        .was_clicked()
812        {
813            events.push(Event::SettingsChange(MapShowPeaks(!show_peaks)));
814        }
815        Text::new(&i18n.get_msg("hud-map-peaks"))
816            .right_from(state.ids.show_peaks_box, 10.0)
817            .font_size(self.fonts.cyri.scale(14))
818            .font_id(self.fonts.cyri.conrod_id)
819            .graphics_for(state.ids.show_peaks_box)
820            .color(TEXT_COLOR)
821            .set(state.ids.show_peaks_text, ui);
822        Text::new(&i18n.get_msg("hud-map-peaks"))
823            .right_from(state.ids.show_peaks_box, 10.0)
824            .font_size(self.fonts.cyri.scale(14))
825            .font_id(self.fonts.cyri.conrod_id)
826            .graphics_for(state.ids.show_peaks_box)
827            .color(TEXT_COLOR)
828            .set(state.ids.show_peaks_text, ui);
829        // Glider Courses
830        Image::new(self.imgs.mmap_site_glider_course)
831            .down_from(state.ids.show_peaks_img, 10.0)
832            .w_h(20.0, 20.0)
833            .set(state.ids.show_glider_courses_img, ui);
834        if Button::image(if show_glider_courses {
835            self.imgs.checkbox_checked
836        } else {
837            self.imgs.checkbox
838        })
839        .w_h(18.0, 18.0)
840        .hover_image(if show_glider_courses {
841            self.imgs.checkbox_checked_mo
842        } else {
843            self.imgs.checkbox_mo
844        })
845        .press_image(if show_glider_courses {
846            self.imgs.checkbox_checked
847        } else {
848            self.imgs.checkbox_press
849        })
850        .right_from(state.ids.show_glider_courses_img, 10.0)
851        .set(state.ids.show_glider_courses_box, ui)
852        .was_clicked()
853        {
854            events.push(Event::SettingsChange(MapShowGliderCourses(
855                !show_glider_courses,
856            )));
857        }
858        Text::new(&i18n.get_msg("hud-map-glider_courses"))
859            .right_from(state.ids.show_glider_courses_box, 10.0)
860            .font_size(self.fonts.cyri.scale(14))
861            .font_id(self.fonts.cyri.conrod_id)
862            .graphics_for(state.ids.show_glider_courses_box)
863            .color(TEXT_COLOR)
864            .set(state.ids.show_glider_courses_text, ui);
865        // Quests
866        Image::new(self.imgs.mmap_unknown)
867            .down_from(state.ids.show_glider_courses_img, 10.0)
868            .w_h(20.0, 20.0)
869            .set(state.ids.show_quests_img, ui);
870        if Button::image(if show_quests {
871            self.imgs.checkbox_checked
872        } else {
873            self.imgs.checkbox
874        })
875        .w_h(18.0, 18.0)
876        .hover_image(if show_quests {
877            self.imgs.checkbox_checked_mo
878        } else {
879            self.imgs.checkbox_mo
880        })
881        .press_image(if show_quests {
882            self.imgs.checkbox_checked
883        } else {
884            self.imgs.checkbox_press
885        })
886        .right_from(state.ids.show_quests_img, 10.0)
887        .set(state.ids.show_quests_box, ui)
888        .was_clicked()
889        {
890            events.push(Event::SettingsChange(MapShowQuests(!show_quests)));
891        }
892        Text::new(&i18n.get_msg("hud-map-quests"))
893            .right_from(state.ids.show_quests_box, 10.0)
894            .font_size(self.fonts.cyri.scale(14))
895            .font_id(self.fonts.cyri.conrod_id)
896            .graphics_for(state.ids.show_quests_box)
897            .color(TEXT_COLOR)
898            .set(state.ids.show_quests_text, ui);
899
900        const EXPOSE_VOXEL_MAP_TOGGLE_IN_UI: bool = false;
901        if EXPOSE_VOXEL_MAP_TOGGLE_IN_UI {
902            Image::new(self.imgs.mmap_poi_peak)
903                .down_from(state.ids.show_peaks_img, 10.0)
904                .w_h(20.0, 20.0)
905                .set(state.ids.show_voxel_map_img, ui);
906            if Button::image(if show_voxel_map {
907                self.imgs.checkbox_checked
908            } else {
909                self.imgs.checkbox
910            })
911            .w_h(18.0, 18.0)
912            .hover_image(if show_voxel_map {
913                self.imgs.checkbox_checked_mo
914            } else {
915                self.imgs.checkbox_mo
916            })
917            .press_image(if show_voxel_map {
918                self.imgs.checkbox_checked
919            } else {
920                self.imgs.checkbox_press
921            })
922            .right_from(state.ids.show_voxel_map_img, 10.0)
923            .set(state.ids.show_voxel_map_box, ui)
924            .was_clicked()
925            {
926                events.push(Event::SettingsChange(MapShowVoxelMap(!show_voxel_map)));
927            }
928            Text::new(&i18n.get_msg("hud-map-voxel_map"))
929                .right_from(state.ids.show_voxel_map_box, 10.0)
930                .font_size(self.fonts.cyri.scale(14))
931                .font_id(self.fonts.cyri.conrod_id)
932                .graphics_for(state.ids.show_voxel_map_box)
933                .color(TEXT_COLOR)
934                .set(state.ids.show_voxel_map_text, ui);
935        }
936        // Map icons
937        if state.ids.mmap_poi_icons.len() < self.client.pois().len() {
938            state.update(|state| {
939                state
940                    .ids
941                    .mmap_poi_icons
942                    .resize(self.client.pois().len(), &mut ui.widget_id_generator())
943            });
944            state.update(|state| {
945                state
946                    .ids
947                    .mmap_poi_titles
948                    .resize(self.client.pois().len(), &mut ui.widget_id_generator())
949            });
950            state.update(|state| {
951                state
952                    .ids
953                    .mmap_poi_title_bgs
954                    .resize(self.client.pois().len(), &mut ui.widget_id_generator())
955            });
956        }
957
958        let markers = self
959            .client
960            .markers()
961            .chain(self.extra_markers.iter().map(|em| &em.marker))
962            .collect::<Vec<_>>();
963
964        if state.ids.mmap_site_icons.len() < markers.len() {
965            state.update(|state| {
966                state
967                    .ids
968                    .mmap_site_icons
969                    .resize(markers.len(), &mut ui.widget_id_generator())
970            });
971        }
972        if state.ids.site_difs.len() < markers.len() {
973            state.update(|state| {
974                state
975                    .ids
976                    .site_difs
977                    .resize(markers.len(), &mut ui.widget_id_generator())
978            });
979        }
980
981        let wpos_to_rpos_fade =
982            |wpos: Vec2<f32>, bounding_rect_size: Vec2<f32>, fade_start: f32| {
983                // Site pos in world coordinates relative to the player
984                let rwpos = wpos - player_pos;
985                // Convert to chunk coordinates
986                let rcpos = rwpos.wpos_to_cpos()
987                // Add map dragging
988                + drag.map(|e| e as f32);
989                // Convert to relative pixel coordinates from the center of the map
990                // Accounting for zooming
991                let rpos = rcpos.map(|e| e * zoom as f32);
992
993                let dist_to_closest_map_edge =
994                    (rpos.map2(map_size, |e, sz| sz as f32 / 2.0 - e.abs()) - bounding_rect_size)
995                        .reduce_partial_min();
996                match dist_to_closest_map_edge {
997                    x if x <= 0.0 => None,
998                    x if x < fade_start => Some((
999                        rpos,
1000                        // Easing function
1001                        1.0 - 2.0_f32.powf(-10.0 * x / fade_start),
1002                    )),
1003                    _ => Some((rpos, 1.0)),
1004                }
1005            };
1006
1007        for (i, marker) in markers.iter().enumerate() {
1008            let rside = zoom as f32 * 8.0 * 1.2;
1009
1010            let (rpos, fade) =
1011                match wpos_to_rpos_fade(marker.wpos, Vec2::from(rside / 2.0), rside / 2.0) {
1012                    Some(rpos) => rpos,
1013                    None => continue,
1014                };
1015
1016            let title = marker
1017                .label
1018                .as_ref()
1019                .map(|name| i18n.get_content(name))
1020                .map(Cow::Owned)
1021                .unwrap_or_else(|| match &marker.kind {
1022                    MarkerKind::Unknown => i18n.get_msg("hud-map-unknown"),
1023                    MarkerKind::Town => i18n.get_msg("hud-map-town"),
1024                    MarkerKind::Castle => i18n.get_msg("hud-map-castle"),
1025                    MarkerKind::Cave => i18n.get_msg("hud-map-cave"),
1026                    MarkerKind::Tree => i18n.get_msg("hud-map-tree"),
1027                    MarkerKind::Gnarling => i18n.get_msg("hud-map-gnarling"),
1028                    MarkerKind::ChapelSite => i18n.get_msg("hud-map-chapel_site"),
1029                    MarkerKind::Terracotta => i18n.get_msg("hud-map-terracotta"),
1030                    MarkerKind::Bridge => i18n.get_msg("hud-map-bridge"),
1031                    MarkerKind::GliderCourse => i18n.get_msg("hud-map-glider_course"),
1032                    MarkerKind::Adlet => i18n.get_msg("hud-map-adlet"),
1033                    MarkerKind::Haniwa => i18n.get_msg("hud-map-haniwa"),
1034                    MarkerKind::Cultist => i18n.get_msg("hud-map-cultist"),
1035                    MarkerKind::Sahagin => i18n.get_msg("hud-map-sahagin"),
1036                    MarkerKind::Myrmidon => i18n.get_msg("hud-map-myrmidon"),
1037                    MarkerKind::DwarvenMine => i18n.get_msg("hud-map-df_mine"),
1038                    MarkerKind::VampireCastle => i18n.get_msg("hud-map-vampire_castle"),
1039                    MarkerKind::Character => i18n.get_msg("hud-map-character"),
1040                });
1041            let (difficulty, desc) = match &marker.kind {
1042                MarkerKind::Unknown => (None, i18n.get_msg("hud-map-unknown")),
1043                MarkerKind::Town => (None, i18n.get_msg("hud-map-town")),
1044                MarkerKind::Castle => (None, i18n.get_msg("hud-map-castle")),
1045                MarkerKind::Cave => (None, i18n.get_msg("hud-map-cave")),
1046                MarkerKind::Tree => (None, i18n.get_msg("hud-map-tree")),
1047                MarkerKind::Gnarling => (Some(0), i18n.get_msg("hud-map-gnarling")),
1048                MarkerKind::Terracotta => (Some(5), i18n.get_msg("hud-map-terracotta")),
1049                MarkerKind::ChapelSite => (Some(4), i18n.get_msg("hud-map-chapel_site")),
1050                MarkerKind::Bridge => (None, i18n.get_msg("hud-map-bridge")),
1051                MarkerKind::GliderCourse => (None, i18n.get_msg("hud-map-glider_course")),
1052                MarkerKind::Adlet => (Some(1), i18n.get_msg("hud-map-adlet")),
1053                MarkerKind::Haniwa => (Some(3), i18n.get_msg("hud-map-haniwa")),
1054                MarkerKind::Cultist => (Some(5), i18n.get_msg("hud-map-cultist")),
1055                MarkerKind::Sahagin => (Some(2), i18n.get_msg("hud-map-sahagin")),
1056                MarkerKind::Myrmidon => (Some(4), i18n.get_msg("hud-map-myrmidon")),
1057                MarkerKind::DwarvenMine => (Some(5), i18n.get_msg("hud-map-df_mine")),
1058                MarkerKind::VampireCastle => (Some(3), i18n.get_msg("hud-map-vampire_castle")),
1059                MarkerKind::Character => (None, i18n.get_msg("hud-map-character")),
1060            };
1061            let desc = if let Some(site_id) = marker.site
1062                && let Some(site) = self.client.sites().get(&site_id)
1063            {
1064                desc.into_owned() + &get_site_economy(site)
1065            } else {
1066                desc.into_owned()
1067            };
1068            let site_btn = Button::image(match &marker.kind {
1069                MarkerKind::Unknown => self.imgs.mmap_unknown,
1070                MarkerKind::Town => self.imgs.mmap_site_town,
1071                MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel,
1072                MarkerKind::Terracotta => self.imgs.mmap_site_terracotta,
1073                MarkerKind::Castle => self.imgs.mmap_site_castle,
1074                MarkerKind::Cave => self.imgs.mmap_site_cave,
1075                MarkerKind::Tree => self.imgs.mmap_site_tree,
1076                MarkerKind::Gnarling => self.imgs.mmap_site_gnarling,
1077                MarkerKind::Adlet => self.imgs.mmap_site_adlet,
1078                MarkerKind::Haniwa => self.imgs.mmap_site_haniwa,
1079                MarkerKind::Cultist => self.imgs.mmap_site_cultist,
1080                MarkerKind::Sahagin => self.imgs.mmap_site_sahagin,
1081                MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon,
1082                MarkerKind::DwarvenMine => self.imgs.mmap_site_mine,
1083                MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle,
1084
1085                MarkerKind::Bridge => self.imgs.mmap_site_bridge,
1086                MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course,
1087                MarkerKind::Character => self.imgs.mmap_character,
1088            })
1089            .x_y_position_relative_to(
1090                state.ids.map_layers[0],
1091                position::Relative::Scalar(rpos.x as f64),
1092                position::Relative::Scalar(rpos.y as f64),
1093            )
1094            .w_h(rside as f64, rside as f64)
1095            .hover_image(match &marker.kind {
1096                MarkerKind::Unknown => self.imgs.mmap_unknown_hover,
1097                MarkerKind::Town => self.imgs.mmap_site_town_hover,
1098                MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel_hover,
1099                MarkerKind::Terracotta => self.imgs.mmap_site_terracotta_hover,
1100                MarkerKind::Castle => self.imgs.mmap_site_castle_hover,
1101                MarkerKind::Cave => self.imgs.mmap_site_cave_hover,
1102                MarkerKind::Tree => self.imgs.mmap_site_tree_hover,
1103                MarkerKind::Gnarling => self.imgs.mmap_site_gnarling_hover,
1104                MarkerKind::Adlet => self.imgs.mmap_site_adlet_hover,
1105                MarkerKind::Haniwa => self.imgs.mmap_site_haniwa_hover,
1106                MarkerKind::Cultist => self.imgs.mmap_site_cultist_hover,
1107                MarkerKind::Sahagin => self.imgs.mmap_site_sahagin_hover,
1108                MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon_hover,
1109                MarkerKind::DwarvenMine => self.imgs.mmap_site_mine_hover,
1110                MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle_hover,
1111                MarkerKind::Bridge => self.imgs.mmap_site_bridge_hover,
1112                MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course_hover,
1113                MarkerKind::Character => self.imgs.mmap_character_hover,
1114            });
1115            let site_btn = if let Some(color) = marker_color(marker, self.pulse) {
1116                site_btn.image_color(color.alpha(fade))
1117            } else {
1118                site_btn.image_color(UI_HIGHLIGHT_0.alpha(fade))
1119            };
1120            let site_btn = site_btn.with_tooltip(
1121                self.tooltip_manager,
1122                &title,
1123                &desc,
1124                &site_tooltip,
1125                match &marker.kind {
1126                    MarkerKind::Gnarling
1127                    | MarkerKind::ChapelSite
1128                    | MarkerKind::Terracotta
1129                    | MarkerKind::Adlet
1130                    | MarkerKind::VampireCastle
1131                    | MarkerKind::Haniwa
1132                    | MarkerKind::Cultist
1133                    | MarkerKind::Sahagin
1134                    | MarkerKind::Myrmidon
1135                    | MarkerKind::DwarvenMine => match difficulty {
1136                        Some(0) => QUALITY_LOW,
1137                        Some(1) => QUALITY_COMMON,
1138                        Some(2) => QUALITY_MODERATE,
1139                        Some(3) => QUALITY_HIGH,
1140                        Some(4 | 5) => QUALITY_EPIC,
1141                        _ => TEXT_COLOR,
1142                    },
1143                    _ => TEXT_COLOR,
1144                },
1145            );
1146
1147            handle_widget_mouse_events(
1148                state.ids.mmap_site_icons[i],
1149                MarkerChange::Pos(marker.wpos),
1150                ui,
1151                &mut events,
1152                state.ids.map_layers[0],
1153            );
1154
1155            // Only display sites that are toggled on
1156            let show_site = match &marker.kind {
1157                _ if marker.flags.contains(MarkerFlags::IS_QUEST) => show_quests,
1158                MarkerKind::Unknown => true,
1159                MarkerKind::Town => show_towns,
1160                MarkerKind::Gnarling
1161                | MarkerKind::ChapelSite
1162                | MarkerKind::DwarvenMine
1163                | MarkerKind::Haniwa
1164                | MarkerKind::Cultist
1165                | MarkerKind::Sahagin
1166                | MarkerKind::Myrmidon
1167                | MarkerKind::Terracotta
1168                | MarkerKind::Adlet
1169                | MarkerKind::VampireCastle => show_dungeons,
1170                MarkerKind::Castle => show_castles,
1171                MarkerKind::Cave => show_caves,
1172                MarkerKind::Tree => show_trees,
1173                MarkerKind::Bridge => show_bridges,
1174                MarkerKind::GliderCourse => show_glider_courses,
1175                MarkerKind::Character => true, // TODO: Toggle for quest markers?
1176            };
1177            if show_site {
1178                let tooltip_visible = site_btn.set_ext(state.ids.mmap_site_icons[i], ui).1;
1179
1180                if SHOW_ECONOMY
1181                    && tooltip_visible
1182                    && let Some(site_id) = marker.site
1183                    && let Some(site) = self.client.sites().get(&site_id)
1184                    && site.economy.is_none()
1185                {
1186                    events.push(Event::RequestSiteInfo(site_id));
1187                }
1188            }
1189
1190            // Difficulty from 0-6
1191            // 0 = towns and places without a difficulty level
1192            if show_difficulty {
1193                let rsize = zoom * 2.4; // Size factor for difficulty indicators
1194                let dif_img = Image::new(match difficulty {
1195                    Some(0) => self.imgs.map_dif_1,
1196                    Some(1) => self.imgs.map_dif_2,
1197                    Some(2) => self.imgs.map_dif_3,
1198                    Some(3) => self.imgs.map_dif_4,
1199                    Some(4 | 5) => self.imgs.map_dif_5,
1200                    Some(_) => self.imgs.map_dif_unknown,
1201                    None => self.imgs.nothing,
1202                })
1203                .mid_top_with_margin_on(state.ids.mmap_site_icons[i], match difficulty {
1204                    Some(0 | 1) => -1.0 * rsize,
1205                    Some(_) => -2.0 * rsize,
1206                    _ => -1.0 * rsize,
1207                })
1208                .w(match difficulty {
1209                    Some(0) => 1.0 * rsize,
1210                    Some(1 | 2) => 2.0 * rsize,
1211                    Some(_) => 3.0 * rsize,
1212                    _ => 1.0 * rsize,
1213                })
1214                .h(match difficulty {
1215                    Some(0 | 1) => 1.0 * rsize,
1216                    Some(_) => 2.0 * rsize,
1217                    _ => 1.0 * rsize,
1218                })
1219                .color(Some(match difficulty {
1220                    Some(0) => QUALITY_LOW,
1221                    Some(1) => QUALITY_COMMON,
1222                    Some(2) => QUALITY_MODERATE,
1223                    Some(3) => QUALITY_HIGH,
1224                    Some(4 | 5) => QUALITY_EPIC, // Change this whenever difficulty is fixed
1225                    _ => TEXT_COLOR,
1226                }));
1227                match &marker.kind {
1228                    MarkerKind::Unknown | MarkerKind::Character => {
1229                        dif_img.set(state.ids.site_difs[i], ui)
1230                    },
1231                    MarkerKind::Town => {
1232                        if show_towns {
1233                            dif_img.set(state.ids.site_difs[i], ui)
1234                        }
1235                    },
1236                    MarkerKind::Gnarling
1237                    | MarkerKind::ChapelSite
1238                    | MarkerKind::Haniwa
1239                    | MarkerKind::Cultist
1240                    | MarkerKind::Sahagin
1241                    | MarkerKind::Myrmidon
1242                    | MarkerKind::Terracotta
1243                    | MarkerKind::Adlet
1244                    | MarkerKind::VampireCastle => {
1245                        if show_dungeons {
1246                            dif_img.set(state.ids.site_difs[i], ui)
1247                        }
1248                    },
1249                    MarkerKind::DwarvenMine => {
1250                        if show_dungeons {
1251                            dif_img.set(state.ids.site_difs[i], ui)
1252                        }
1253                    },
1254                    MarkerKind::Castle => {
1255                        if show_castles {
1256                            dif_img.set(state.ids.site_difs[i], ui)
1257                        }
1258                    },
1259                    MarkerKind::Cave => {
1260                        if show_caves {
1261                            dif_img.set(state.ids.site_difs[i], ui)
1262                        }
1263                    },
1264                    MarkerKind::Tree => {
1265                        if show_trees {
1266                            dif_img.set(state.ids.site_difs[i], ui)
1267                        }
1268                    },
1269                    MarkerKind::Bridge => {
1270                        if show_bridges {
1271                            dif_img.set(state.ids.site_difs[i], ui)
1272                        }
1273                    },
1274                    MarkerKind::GliderCourse => {
1275                        if show_glider_courses {
1276                            dif_img.set(state.ids.site_difs[i], ui)
1277                        }
1278                    },
1279                }
1280
1281                handle_widget_mouse_events(
1282                    state.ids.site_difs[i],
1283                    MarkerChange::Pos(marker.wpos),
1284                    ui,
1285                    &mut events,
1286                    state.ids.map_layers[0],
1287                );
1288            }
1289        }
1290        for (i, poi) in self.client.pois().iter().enumerate() {
1291            // TODO: computation of text size to pass to wpos_to_rpos_fade, so it can
1292            // determine when it's going past the edge of the map screen
1293            let (rpos, fade) = match wpos_to_rpos_fade(
1294                poi.wpos.map(|e| e as f32),
1295                Vec2::from(zoom as f32 * 3.0),
1296                zoom as f32 * 5.0,
1297            ) {
1298                Some(rpos) => rpos,
1299                None => continue,
1300            };
1301            let title = &poi.name;
1302            match poi.kind {
1303                PoiKind::Peak(alt) => {
1304                    let height = format!("{} m", alt);
1305                    if show_peaks && zoom > 2.0 {
1306                        Text::new(title)
1307                            .x_y_position_relative_to(
1308                                state.ids.map_layers[0],
1309                                position::Relative::Scalar(rpos.x as f64),
1310                                position::Relative::Scalar(rpos.y as f64 + zoom * 4.0),
1311                            )
1312                            .font_size(self.fonts.cyri.scale((zoom * 3.0) as u32))
1313                            .font_id(self.fonts.cyri.conrod_id)
1314                            .graphics_for(state.ids.map_layers[0])
1315                            .color(TEXT_BG.alpha(fade))
1316                            .set(state.ids.mmap_poi_title_bgs[i], ui);
1317                        Text::new(title)
1318                                .bottom_left_with_margins_on(state.ids.mmap_poi_title_bgs[i], 1.0, 1.0)
1319                                .font_size(self.fonts.cyri.scale((zoom * 3.0) as u32))
1320                                .font_id(self.fonts.cyri.conrod_id)
1321                                //.graphics_for(state.ids.map_layers[0])
1322                                .color(TEXT_COLOR.alpha(fade))
1323                                .set(state.ids.mmap_poi_titles[i], ui);
1324
1325                        handle_widget_mouse_events(
1326                            state.ids.mmap_poi_titles[i],
1327                            MarkerChange::Pos(poi.wpos.map(|e| e as f32)),
1328                            ui,
1329                            &mut events,
1330                            state.ids.map_layers[0],
1331                        );
1332
1333                        // Show peak altitude
1334                        if ui
1335                            .widget_input(state.ids.mmap_poi_titles[i])
1336                            .mouse()
1337                            .is_some_and(|m| m.is_over())
1338                        {
1339                            Text::new(&height)
1340                                .mid_bottom_with_margin_on(
1341                                    state.ids.mmap_poi_title_bgs[i],
1342                                    zoom * 3.5,
1343                                )
1344                                .font_size(self.fonts.cyri.scale((zoom * 3.0) as u32))
1345                                .font_id(self.fonts.cyri.conrod_id)
1346                                .graphics_for(state.ids.map_layers[0])
1347                                .color(TEXT_BG.alpha(fade))
1348                                .set(state.ids.peaks_txt_bg, ui);
1349                            Text::new(&height)
1350                                .bottom_left_with_margins_on(state.ids.peaks_txt_bg, 1.0, 1.0)
1351                                .font_size(self.fonts.cyri.scale((zoom * 3.0) as u32))
1352                                .font_id(self.fonts.cyri.conrod_id)
1353                                .graphics_for(state.ids.map_layers[0])
1354                                .color(TEXT_COLOR.alpha(fade))
1355                                .set(state.ids.peaks_txt, ui);
1356                        }
1357                    }
1358                },
1359                PoiKind::Lake(size) => {
1360                    if show_biomes && zoom > 2.0 && zoom.powi(2) * size as f64 > 30.0 {
1361                        let font_scale_factor = if size > 20 {
1362                            size as f64 / 25.0
1363                        } else if size > 10 {
1364                            size as f64 / 10.0
1365                        } else if size > 5 {
1366                            size as f64 / 6.0
1367                        } else {
1368                            size as f64 / 2.5
1369                        };
1370                        Text::new(title)
1371                            .x_y_position_relative_to(
1372                                state.ids.map_layers[0],
1373                                position::Relative::Scalar(rpos.x as f64),
1374                                position::Relative::Scalar(rpos.y as f64),
1375                            )
1376                            .font_size(
1377                                self.fonts.cyri.scale(
1378                                    (2.0 + font_scale_factor * zoom).clamp(10.0, 18.0) as u32,
1379                                ),
1380                            )
1381                            .font_id(self.fonts.cyri.conrod_id)
1382                            .graphics_for(state.ids.map_layers[0])
1383                            .color(TEXT_BLUE_COLOR.alpha(fade))
1384                            .set(state.ids.mmap_poi_icons[i], ui);
1385                    }
1386                },
1387            }
1388        }
1389        // Group member indicators
1390        let client_state = self.client.state();
1391        let stats = client_state.ecs().read_storage::<comp::Stats>();
1392        let member_pos = client_state.ecs().read_storage::<comp::Pos>();
1393        let group_members = self
1394            .client
1395            .group_members()
1396            .iter()
1397            .filter_map(|(u, r)| match r {
1398                Role::Member => Some(u),
1399                Role::Pet => None,
1400            })
1401            .collect::<Vec<_>>();
1402        let group_size = group_members.len();
1403        //let in_group = !group_members.is_empty();
1404        let id_maps = client_state
1405            .ecs()
1406            .read_resource::<common_net::sync::IdMaps>();
1407        if state.ids.member_indicators.len() < group_size {
1408            state.update(|s| {
1409                s.ids
1410                    .member_indicators
1411                    .resize(group_size, &mut ui.widget_id_generator())
1412            })
1413        };
1414        for (i, &uid) in group_members.iter().copied().enumerate() {
1415            let entity = id_maps.uid_entity(uid);
1416            let member_pos = entity.and_then(|entity| member_pos.get(entity));
1417            let stats = entity.and_then(|entity| stats.get(entity));
1418            let name = if let Some(stats) = stats {
1419                i18n.get_content(&stats.name)
1420            } else {
1421                "".to_string()
1422            };
1423
1424            if let Some(member_pos) = member_pos {
1425                let factor = 1.2;
1426                let side_length = 20.0 * factor;
1427
1428                let (rpos, fade) = match wpos_to_rpos_fade(
1429                    member_pos.0.xy(),
1430                    Vec2::from(side_length / 2.0),
1431                    side_length / 2.0,
1432                ) {
1433                    Some(x) => x,
1434                    None => continue,
1435                };
1436
1437                let z_comparison = (member_pos.0.z - player_pos.z) as i32;
1438
1439                Button::image(match z_comparison {
1440                    10..=i32::MAX => self.imgs.indicator_group_up,
1441                    i32::MIN..=-10 => self.imgs.indicator_group_down,
1442                    _ => self.imgs.indicator_group,
1443                })
1444                .x_y_position_relative_to(
1445                    state.ids.map_layers[0],
1446                    position::Relative::Scalar(rpos.x as f64),
1447                    position::Relative::Scalar(rpos.y as f64),
1448                )
1449                .w_h(side_length as f64, side_length as f64)
1450                .image_color(Color::Rgba(1.0, 1.0, 1.0, fade))
1451                .floating(true)
1452                .with_tooltip(self.tooltip_manager, &name, "", &site_tooltip, TEXT_COLOR)
1453                .set(state.ids.member_indicators[i], ui);
1454
1455                handle_widget_mouse_events(
1456                    state.ids.member_indicators[i],
1457                    MarkerChange::Pos(member_pos.0.xy()),
1458                    ui,
1459                    &mut events,
1460                    state.ids.map_layers[0],
1461                );
1462            }
1463        }
1464
1465        let factor = 1.4;
1466        let side_length = 20.0 * factor;
1467        // Groups location markers
1468        if state.ids.location_marker_group.len() < self.location_markers.group.len() {
1469            state.update(|s| {
1470                s.ids.location_marker_group.resize(
1471                    self.location_markers.group.len(),
1472                    &mut ui.widget_id_generator(),
1473                )
1474            })
1475        };
1476        for (i, (&uid, &rpos)) in self.location_markers.group.iter().enumerate() {
1477            let lm = rpos.as_();
1478            if let Some((rpos, fade)) =
1479                wpos_to_rpos_fade(lm, Vec2::from(side_length / 2.0), side_length / 2.0)
1480            {
1481                let name = self
1482                    .client
1483                    .player_list()
1484                    .get(&uid)
1485                    .map(|info| info.player_alias.clone())
1486                    .or_else(|| {
1487                        id_maps
1488                            .uid_entity(uid)
1489                            .and_then(|entity| stats.get(entity))
1490                            .map(|stats| i18n.get_content(&stats.name))
1491                    })
1492                    .unwrap_or(String::new());
1493
1494                let image_id = match self.client.group_info().map(|info| info.1) {
1495                    Some(leader) if leader == uid => self.imgs.location_marker_group_leader,
1496                    _ => self.imgs.location_marker_group,
1497                };
1498
1499                Button::image(image_id)
1500                    .x_y_position_relative_to(
1501                        state.ids.map_layers[0],
1502                        position::Relative::Scalar(rpos.x as f64),
1503                        position::Relative::Scalar(rpos.y as f64 + 10.0 * factor as f64),
1504                    )
1505                    .w_h(side_length as f64, side_length as f64)
1506                    .image_color(Color::Rgba(1.0, 1.0, 1.0, fade))
1507                    .floating(true)
1508                    .with_tooltip(
1509                        self.tooltip_manager,
1510                        &i18n.get_msg("hud-map-marked_location"),
1511                        &format!(
1512                            "X: {}, Y: {}\n\n{}",
1513                            lm.x as i32,
1514                            lm.y as i32,
1515                            i18n.get_msg_ctx("hud-map-placed_by", &i18n::fluent_args! {
1516                                "name" => name
1517                            }),
1518                        ),
1519                        &site_tooltip,
1520                        TEXT_VELORITE,
1521                    )
1522                    .set(state.ids.location_marker_group[i], ui);
1523                handle_widget_mouse_events(
1524                    state.ids.location_marker_group[i],
1525                    MarkerChange::Pos(lm),
1526                    ui,
1527                    &mut events,
1528                    state.ids.map_layers[0],
1529                );
1530            }
1531        }
1532        // Location marker
1533        if let Some((lm, (rpos, fade))) = self.location_markers.owned.and_then(|lm| {
1534            let lm = lm.as_();
1535            Some(lm).zip(wpos_to_rpos_fade(
1536                lm,
1537                Vec2::from(side_length / 2.0),
1538                side_length / 2.0,
1539            ))
1540        }) {
1541            if Button::image(self.imgs.location_marker)
1542                .x_y_position_relative_to(
1543                    state.ids.map_layers[0],
1544                    position::Relative::Scalar(rpos.x as f64),
1545                    position::Relative::Scalar(rpos.y as f64 + 10.0 * factor as f64),
1546                )
1547                .w_h(side_length as f64, side_length as f64)
1548                .image_color(Color::Rgba(1.0, 1.0, 1.0, fade))
1549                .floating(true)
1550                .with_tooltip(
1551                    self.tooltip_manager,
1552                    &i18n.get_msg("hud-map-marked_location"),
1553                    &format!(
1554                        "X: {}, Y: {}\n\n{}",
1555                        lm.x as i32,
1556                        lm.y as i32,
1557                        i18n.get_msg("hud-map-marked_location_remove")
1558                    ),
1559                    &site_tooltip,
1560                    TEXT_VELORITE,
1561                )
1562                .set(state.ids.location_marker, ui)
1563                .was_clicked()
1564            {
1565                events.push(Event::RemoveMarker);
1566            }
1567
1568            handle_widget_mouse_events(
1569                state.ids.location_marker,
1570                MarkerChange::Remove,
1571                ui,
1572                &mut events,
1573                state.ids.map_layers[0],
1574            );
1575        }
1576
1577        // Cursor pos relative to playerpos and widget size
1578        // Cursor stops moving on an axis as soon as it's position exceeds the maximum
1579        // // size of the widget
1580
1581        // Don't show if outside or near the edge of the map
1582        let arrow_sz = {
1583            let scale = 0.5;
1584            Vec2::new(36.0, 37.0) * scale
1585        };
1586        // Hide if icon could go off of the edge of the map
1587        if let Some((rpos, fade)) =
1588            wpos_to_rpos_fade(player_pos.xy(), arrow_sz, arrow_sz.reduce_partial_min())
1589        {
1590            let ind = {
1591                if colored_player_marker {
1592                    self.rot_imgs.indicator_mmap_colored.target_north
1593                } else {
1594                    self.rot_imgs.indicator_mmap.target_north
1595                }
1596            };
1597            Image::new(ind)
1598                .x_y_position_relative_to(
1599                    state.ids.map_layers[0],
1600                    position::Relative::Scalar(rpos.x as f64),
1601                    position::Relative::Scalar(rpos.y as f64),
1602                )
1603                .w_h(arrow_sz.x as f64, arrow_sz.y as f64)
1604                .color(Some(UI_HIGHLIGHT_0.alpha(fade)))
1605                .set(state.ids.indicator, ui);
1606
1607            handle_widget_mouse_events(
1608                state.ids.indicator,
1609                MarkerChange::Pos(player_pos.xy()),
1610                ui,
1611                &mut events,
1612                state.ids.map_layers[0],
1613            );
1614        }
1615
1616        // Info about controls
1617        let icon_size = Vec2::new(25.6, 28.8);
1618        let recenter: bool = drag.x != 0.0 || drag.y != 0.0;
1619        if Button::image(self.imgs.button)
1620            .w_h(92.0, icon_size.y)
1621            .mid_bottom_with_margin_on(state.ids.map_layers[0], -36.0)
1622            .hover_image(if recenter {
1623                self.imgs.button_hover
1624            } else {
1625                self.imgs.button
1626            })
1627            .press_image(if recenter {
1628                self.imgs.button_press
1629            } else {
1630                self.imgs.button
1631            })
1632            .label(&i18n.get_msg("hud-map-recenter"))
1633            .label_y(position::Relative::Scalar(1.0))
1634            .label_color(if recenter {
1635                TEXT_COLOR
1636            } else {
1637                TEXT_GRAY_COLOR
1638            })
1639            .image_color(if recenter {
1640                TEXT_COLOR
1641            } else {
1642                TEXT_GRAY_COLOR
1643            })
1644            .label_font_size(self.fonts.cyri.scale(12))
1645            .label_font_id(self.fonts.cyri.conrod_id)
1646            .set(state.ids.recenter_button, ui)
1647            .was_clicked()
1648        {
1649            events.push(Event::MapDrag(Vec2::zero()));
1650        };
1651
1652        Image::new(self.imgs.m_move_ico)
1653            .bottom_left_with_margins_on(state.ids.map_layers[0], -36.0, 0.0)
1654            .w_h(icon_size.x, icon_size.y)
1655            .color(Some(UI_HIGHLIGHT_0))
1656            .set(state.ids.drag_ico, ui);
1657        Text::new(&i18n.get_msg("hud-map-drag"))
1658            .right_from(state.ids.drag_ico, 5.0)
1659            .font_size(self.fonts.cyri.scale(14))
1660            .font_id(self.fonts.cyri.conrod_id)
1661            .graphics_for(state.ids.map_layers[0])
1662            .color(TEXT_COLOR)
1663            .set(state.ids.drag_txt, ui);
1664        Image::new(self.imgs.m_scroll_ico)
1665            .right_from(state.ids.drag_txt, 5.0)
1666            .w_h(icon_size.x, icon_size.y)
1667            .color(Some(UI_HIGHLIGHT_0))
1668            .set(state.ids.zoom_ico, ui);
1669        Text::new(&i18n.get_msg("hud-map-zoom"))
1670            .right_from(state.ids.zoom_ico, 5.0)
1671            .font_size(self.fonts.cyri.scale(14))
1672            .font_id(self.fonts.cyri.conrod_id)
1673            .graphics_for(state.ids.map_layers[0])
1674            .color(TEXT_COLOR)
1675            .set(state.ids.zoom_txt, ui);
1676
1677        Text::new(&location_marker_binding.display_shortest())
1678            .right_from(state.ids.zoom_txt, 15.0)
1679            .font_size(self.fonts.cyri.scale(14))
1680            .font_id(self.fonts.cyri.conrod_id)
1681            .graphics_for(state.ids.map_layers[0])
1682            .color(TEXT_COLOR)
1683            .set(state.ids.waypoint_binding_txt, ui);
1684
1685        Text::new(&i18n.get_msg("hud-map-mid_click"))
1686            .right_from(state.ids.waypoint_binding_txt, 5.0)
1687            .font_size(self.fonts.cyri.scale(14))
1688            .font_id(self.fonts.cyri.conrod_id)
1689            .graphics_for(state.ids.map_layers[0])
1690            .color(TEXT_COLOR)
1691            .set(state.ids.waypoint_txt, ui);
1692
1693        // Show topographic map
1694        if Button::image(self.imgs.button)
1695            .w_h(92.0, icon_size.y)
1696            .hover_image(self.imgs.button_hover)
1697            .press_image(self.imgs.button_press)
1698            .bottom_right_with_margins_on(state.ids.map_layers[0], -36.0, 0.0)
1699            .with_tooltip(
1700                self.tooltip_manager,
1701                &i18n.get_msg("hud-map-change_map_mode"),
1702                "",
1703                &site_tooltip,
1704                TEXT_COLOR,
1705            )
1706            .set(state.ids.map_mode_btn, ui)
1707            .was_clicked()
1708        {
1709            events.push(Event::SettingsChange(MapShowTopoMap(!show_topo_map)));
1710        };
1711        Button::image(self.imgs.map_mode_overlay)
1712            .w_h(92.0, icon_size.y)
1713            .graphics_for(state.ids.map_mode_btn)
1714            .middle_of(state.ids.map_mode_btn)
1715            .set(state.ids.map_mode_overlay, ui);
1716
1717        // Render voxel view on minimap
1718        if Button::image(self.imgs.button)
1719            .w_h(92.0, icon_size.y)
1720            .hover_image(self.imgs.button_hover)
1721            .press_image(self.imgs.button_press)
1722            .left_from(state.ids.map_mode_btn, 5.0)
1723            .with_tooltip(
1724                self.tooltip_manager,
1725                &i18n.get_msg("hud-map-toggle_minimap_voxel"),
1726                &i18n.get_msg("hud-map-zoom_minimap_explanation"),
1727                &site_tooltip,
1728                TEXT_COLOR,
1729            )
1730            .set(state.ids.minimap_mode_btn, ui)
1731            .was_clicked()
1732        {
1733            events.push(Event::SettingsChange(MapShowVoxelMap(!show_voxel_map)));
1734        };
1735        Button::image(self.imgs.minimap_mode_overlay)
1736            .w_h(92.0, icon_size.y)
1737            .graphics_for(state.ids.minimap_mode_btn)
1738            .middle_of(state.ids.minimap_mode_btn)
1739            .set(state.ids.minimap_mode_overlay, ui);
1740
1741        events
1742    }
1743}
1744
1745pub fn marker_color(marker: &Marker, pulse: f32) -> Option<Color> {
1746    if marker.flags.contains(MarkerFlags::IS_QUEST) {
1747        Some(Color::Rgba(
1748            0.3,
1749            1.0,
1750            0.85,
1751            0.75 + (pulse * 4.0).sin() * 0.25,
1752        ))
1753    } else {
1754        None
1755    }
1756}