veloren_voxygen/hud/
minimap.rs

1use super::{
2    MapMarkers, QUALITY_COMMON, QUALITY_DEBUG, QUALITY_EPIC, QUALITY_HIGH, QUALITY_LOW,
3    QUALITY_MODERATE, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
4    img_ids::{Imgs, ImgsRot},
5};
6use crate::{
7    GlobalState,
8    hud::{Graphic, Ui},
9    session::settings_change::Interface::{self as InterfaceChange, *},
10    settings::HudPositionSettings,
11    ui::{KeyedJobs, fonts::Fonts, img_ids},
12};
13use client::{self, Client};
14use common::{
15    comp,
16    comp::group::Role,
17    grid::Grid,
18    map::{MarkerFlags, MarkerKind},
19    slowjob::SlowJobPool,
20    terrain::{
21        Block, BlockKind, CoordinateConversions, TerrainChunk, TerrainChunkSize, TerrainGrid,
22    },
23    vol::{ReadVol, RectVolSize},
24};
25use common_state::TerrainChanges;
26use conrod_core::{
27    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color, position,
28    widget::{self, Button, Image, Rectangle, Text},
29    widget_ids,
30};
31use hashbrown::{HashMap, HashSet};
32use image::{DynamicImage, RgbaImage};
33use specs::WorldExt;
34use std::sync::Arc;
35
36use vek::{Rgba, Vec2, Vec3, approx::AbsDiffEq};
37
38struct MinimapColumn {
39    /// Coordinate of lowest z-slice
40    zlo: i32,
41    /// Z-slices of colors and filled-ness
42    layers: Vec<Grid<(Rgba<u8>, bool)>>,
43    /// Color and filledness above the highest layer
44    above: (Rgba<u8>, bool),
45    /// Color and filledness below the lowest layer
46    below: (Rgba<u8>, bool),
47}
48
49pub struct VoxelMinimap {
50    chunk_minimaps: HashMap<Vec2<i32>, MinimapColumn>,
51    chunks_to_replace: HashSet<Vec2<i32>>,
52    composited: RgbaImage,
53    image_id: img_ids::Rotations,
54    last_pos: Vec3<i32>,
55    last_ceiling: i32,
56    keyed_jobs: KeyedJobs<Vec2<i32>, MinimapColumn>,
57}
58
59const VOXEL_MINIMAP_SIDELENGTH: u32 = 256;
60
61impl VoxelMinimap {
62    pub fn new(ui: &mut Ui) -> Self {
63        let composited = RgbaImage::from_pixel(
64            VOXEL_MINIMAP_SIDELENGTH,
65            VOXEL_MINIMAP_SIDELENGTH,
66            image::Rgba([0, 0, 0, 64]),
67        );
68        Self {
69            chunk_minimaps: HashMap::new(),
70            chunks_to_replace: HashSet::new(),
71            image_id: ui.add_graphic_with_rotations(Graphic::Image(
72                Arc::new(DynamicImage::ImageRgba8(composited.clone())),
73                Some(Rgba::from([0.0, 0.0, 0.0, 0.0])),
74            )),
75            composited,
76            last_pos: Vec3::zero(),
77            last_ceiling: 0,
78            keyed_jobs: KeyedJobs::new("IMAGE_PROCESSING"),
79        }
80    }
81
82    fn block_color(block: &Block) -> Option<Rgba<u8>> {
83        block
84            .get_color()
85            .map(|rgb| Rgba::new(rgb.r, rgb.g, rgb.b, 255))
86            .or_else(|| {
87                matches!(block.kind(), BlockKind::Water).then(|| Rgba::new(119, 149, 197, 255))
88            })
89    }
90
91    /// Each layer is a slice of the terrain near that z-level
92    fn composite_layer_slice(chunk: &TerrainChunk, layers: &mut Vec<Grid<(Rgba<u8>, bool)>>) {
93        for z in chunk.get_min_z()..=chunk.get_max_z() {
94            let grid = Grid::populate_from(Vec2::new(32, 32), |v| {
95                let mut rgba = Rgba::<f32>::zero();
96                let (weights, zoff) = (&[1, 2, 4, 1, 1, 1][..], -2);
97                for (dz, weight) in weights.iter().enumerate() {
98                    let color = chunk
99                        .get(Vec3::new(v.x, v.y, dz as i32 + z + zoff))
100                        .ok()
101                        .and_then(Self::block_color)
102                        .unwrap_or_else(Rgba::zero);
103                    rgba += color.as_() * *weight as f32;
104                }
105                let rgba: Rgba<u8> = (rgba / weights.iter().map(|x| *x as f32).sum::<f32>()).as_();
106                (rgba, true)
107            });
108            layers.push(grid);
109        }
110    }
111
112    /// Each layer is the overhead as if its z-level were the ceiling
113    fn composite_layer_overhead(chunk: &TerrainChunk, layers: &mut Vec<Grid<(Rgba<u8>, bool)>>) {
114        for z in chunk.get_min_z()..=chunk.get_max_z() {
115            let grid = Grid::populate_from(Vec2::new(32, 32), |v| {
116                let mut rgba = None;
117
118                let mut seen_solids: u32 = 0;
119                let mut seen_air: u32 = 0;
120                for dz in chunk.get_min_z()..=z {
121                    if let Some(color) = chunk
122                        .get(Vec3::new(v.x, v.y, z - dz + chunk.get_min_z()))
123                        .ok()
124                        .and_then(Self::block_color)
125                    {
126                        if seen_air > 0 {
127                            rgba = Some(color);
128                            break;
129                        }
130                        seen_solids += 1;
131                    } else {
132                        seen_air += 1;
133                    }
134                    // Don't penetrate too far into ground, only penetrate through shallow
135                    // ceilings
136                    if seen_solids > 12 {
137                        break;
138                    }
139                }
140                let block = chunk.get(Vec3::new(v.x, v.y, z)).ok();
141                // Treat Leaves and Wood as translucent for the purposes of ceiling checks,
142                // since otherwise trees would cause ceiling removal to trigger
143                // when running under a branch.
144                let is_filled = block.is_none_or(|b| {
145                    b.is_filled()
146                        && !matches!(
147                            b.kind(),
148                            BlockKind::Leaves | BlockKind::ArtLeaves | BlockKind::Wood
149                        )
150                });
151                let rgba = rgba.unwrap_or_else(|| Rgba::new(0, 0, 0, 255));
152                (rgba, is_filled)
153            });
154            layers.push(grid);
155        }
156    }
157
158    fn add_chunks_near(
159        &mut self,
160        pool: &SlowJobPool,
161        terrain: &TerrainGrid,
162        cpos: Vec2<i32>,
163    ) -> bool {
164        let mut new_chunks = false;
165
166        for (key, chunk) in terrain.iter() {
167            let delta: Vec2<u32> = (key - cpos).map(i32::abs).as_();
168            if delta.x < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x
169                && delta.y < VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y
170                && (!self.chunk_minimaps.contains_key(&key)
171                    || self.chunks_to_replace.contains(&key))
172                && let Some((_, column)) = self.keyed_jobs.spawn(Some(pool), key, || {
173                    let arc_chunk = Arc::clone(chunk);
174                    move |_| {
175                        let mut layers = Vec::new();
176                        const MODE_OVERHEAD: bool = true;
177                        if MODE_OVERHEAD {
178                            Self::composite_layer_overhead(&arc_chunk, &mut layers);
179                        } else {
180                            Self::composite_layer_slice(&arc_chunk, &mut layers);
181                        }
182                        let above = arc_chunk
183                            .get(Vec3::new(0, 0, arc_chunk.get_max_z() + 1))
184                            .ok()
185                            .copied()
186                            .unwrap_or_else(Block::empty);
187                        let below = arc_chunk
188                            .get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1))
189                            .ok()
190                            .copied()
191                            .unwrap_or_else(Block::empty);
192                        MinimapColumn {
193                            zlo: arc_chunk.get_min_z(),
194                            layers,
195                            above: (
196                                Self::block_color(&above).unwrap_or_else(Rgba::zero),
197                                above.is_filled(),
198                            ),
199                            below: (
200                                Self::block_color(&below).unwrap_or_else(Rgba::zero),
201                                below.is_filled(),
202                            ),
203                        }
204                    }
205                })
206            {
207                self.chunks_to_replace.remove(&key);
208                self.chunk_minimaps.insert(key, column);
209                new_chunks = true;
210            }
211        }
212        new_chunks
213    }
214
215    fn add_chunks_to_replace(&mut self, terrain: &TerrainGrid, changes: &TerrainChanges) {
216        changes
217            .modified_blocks
218            .iter()
219            .filter(|(key, old_block)| {
220                terrain
221                    .get(**key)
222                    .is_ok_and(|new_block| new_block.is_terrain() != old_block.is_terrain())
223            })
224            .map(|(key, _)| terrain.pos_key(*key))
225            .for_each(|key| {
226                self.chunks_to_replace.insert(key);
227            });
228    }
229
230    fn remove_chunks_far(&mut self, terrain: &TerrainGrid, cpos: Vec2<i32>) {
231        let key_predicate = |key: &Vec2<i32>| {
232            let delta: Vec2<u32> = (key - cpos).map(i32::abs).as_();
233            delta.x < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x
234                && delta.y < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y
235                && terrain.get_key(*key).is_some()
236        };
237        self.chunks_to_replace.retain(&key_predicate);
238        self.chunk_minimaps.retain(|key, _| key_predicate(key));
239    }
240
241    pub fn maintain(&mut self, client: &Client, ui: &mut Ui) {
242        let player = client.entity();
243        let pos = if let Some(pos) = client.state().ecs().read_storage::<comp::Pos>().get(player) {
244            pos.0
245        } else {
246            return;
247        };
248        let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0;
249        let cpos: Vec2<i32> = vpos
250            .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
251            .as_();
252
253        let pool = client.state().ecs().read_resource::<SlowJobPool>();
254        let terrain = client.state().terrain();
255        let changed_blocks = client.state().terrain_changes();
256        self.add_chunks_to_replace(&terrain, &changed_blocks);
257        let new_chunks = self.add_chunks_near(&pool, &terrain, cpos);
258        self.remove_chunks_far(&terrain, cpos);
259
260        // ceiling_offset is the distance from the player to a block heuristically
261        // detected as the ceiling height (a non-tree solid block above them, or
262        // the sky if no such block exists). This is used for determining which
263        // z-slice of the minimap to show, such that house roofs and caves and
264        // dungeons are all handled uniformly.
265        let ceiling_offset = {
266            let voff = Vec2::new(
267                VOXEL_MINIMAP_SIDELENGTH as f32,
268                VOXEL_MINIMAP_SIDELENGTH as f32,
269            ) / 2.0;
270            let coff: Vec2<i32> = voff
271                .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
272                .as_();
273            let cmod: Vec2<i32> = vpos
274                .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
275                .as_();
276            let column = self.chunk_minimaps.get(&(cpos + coff));
277            // TODO: evaluate clippy, toolchain upgrade 2021-12-19
278            #[expect(clippy::unnecessary_lazy_evaluations)]
279            column
280                .map(
281                    |MinimapColumn {
282                         zlo, layers, above, ..
283                     }| {
284                        (0..layers.len() as i32)
285                            .find(|dz| {
286                                layers
287                                    .get((pos.z as i32 - zlo + dz) as usize)
288                                    .and_then(|grid| grid.get(cmod)).is_some_and(|(_, b)| *b)
289                            })
290                            .unwrap_or_else(||
291                                // if the `find` returned None, there's no solid blocks above the
292                                // player within the chunk
293                                if above.1 {
294                                    // if the `above` block is solid, the chunk has an infinite
295                                    // solid ceiling, and so we render from 1 block above the
296                                    // player (which is where the player's head is if they're 2
297                                    // blocks tall)
298                                    1
299                                } else {
300                                    // if the ceiling is a non-solid sky, use the largest value
301                                    // (subsequent arithmetic on ceiling_offset must be saturating)
302                                    i32::MAX
303                                }
304                            )
305                    },
306                )
307                .unwrap_or(0)
308        };
309        if self.last_pos.xy() != cpos
310            || self.last_pos.z != pos.z as i32
311            || self.last_ceiling != ceiling_offset
312            || new_chunks
313        {
314            self.last_pos = cpos.with_z(pos.z as i32);
315            self.last_ceiling = ceiling_offset;
316            for y in 0..VOXEL_MINIMAP_SIDELENGTH {
317                for x in 0..VOXEL_MINIMAP_SIDELENGTH {
318                    let voff = Vec2::new(x as f32, y as f32);
319                    let coff: Vec2<i32> = voff
320                        .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
321                        .as_();
322                    let cmod: Vec2<i32> = voff
323                        .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
324                        .as_();
325                    let column = self.chunk_minimaps.get(&(cpos + coff));
326                    let color: Rgba<u8> = column
327                        .and_then(|column| {
328                            let MinimapColumn {
329                                zlo,
330                                layers,
331                                above,
332                                below,
333                            } = column;
334                            if (pos.z as i32).saturating_add(ceiling_offset) < *zlo {
335                                // If the ceiling is below the bottom of a chunk, color it black,
336                                // so that the middles of caves/dungeons don't show the forests
337                                // around them.
338                                Some(Rgba::new(0, 0, 0, 255))
339                            } else {
340                                // Otherwise, take the pixel from the precomputed z-level view at
341                                // the ceiling's height (using the top slice of the chunk if the
342                                // ceiling is above the chunk, (e.g. so that forests with
343                                // differently-tall trees are handled properly)
344                                // TODO: evaluate clippy, toolchain upgrade 2021-12-19
345                                #[expect(clippy::unnecessary_lazy_evaluations)]
346                                layers
347                                    .get(
348                                        (((pos.z as i32 - zlo).saturating_add(ceiling_offset))
349                                            as usize)
350                                            .min(layers.len().saturating_sub(1)),
351                                    )
352                                    .and_then(|grid| grid.get(cmod).map(|c| c.0.as_()))
353                                    .or_else(|| {
354                                        Some(if pos.z as i32 > *zlo {
355                                            above.0
356                                        } else {
357                                            below.0
358                                        })
359                                    })
360                            }
361                        })
362                        .unwrap_or_else(Rgba::zero);
363                    self.composited.put_pixel(
364                        x,
365                        VOXEL_MINIMAP_SIDELENGTH - y - 1,
366                        image::Rgba([color.r, color.g, color.b, color.a]),
367                    );
368                }
369            }
370
371            ui.replace_graphic(
372                self.image_id.none,
373                Graphic::Image(
374                    Arc::new(DynamicImage::ImageRgba8(self.composited.clone())),
375                    Some(Rgba::from([0.0, 0.0, 0.0, 0.0])),
376                ),
377            );
378        }
379    }
380}
381
382widget_ids! {
383    struct Ids {
384        mmap_frame,
385        mmap_frame_2,
386        mmap_frame_bg,
387        mmap_location,
388        mmap_coordinates,
389        mmap_button,
390        mmap_plus,
391        mmap_minus,
392        mmap_north_button,
393        map_layers[],
394        camera,
395        indicator,
396        mmap_north,
397        mmap_east,
398        mmap_south,
399        mmap_west,
400        mmap_site_icons_bgs[],
401        mmap_site_icons[],
402        member_indicators[],
403        location_marker,
404        location_marker_group[],
405        voxel_minimap,
406        draggable_area,
407    }
408}
409
410#[derive(WidgetCommon)]
411pub struct MiniMap<'a> {
412    client: &'a Client,
413    imgs: &'a Imgs,
414    rot_imgs: &'a ImgsRot,
415    world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
416    fonts: &'a Fonts,
417    pulse: f32,
418    #[conrod(common_builder)]
419    common: widget::CommonBuilder,
420    ori: Vec3<f32>,
421    global_state: &'a GlobalState,
422    location_markers: &'a MapMarkers,
423    voxel_minimap: &'a VoxelMinimap,
424    extra_markers: &'a [super::map::ExtraMarker],
425}
426
427impl<'a> MiniMap<'a> {
428    pub fn new(
429        client: &'a Client,
430        imgs: &'a Imgs,
431        rot_imgs: &'a ImgsRot,
432        world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
433        fonts: &'a Fonts,
434        pulse: f32,
435        ori: Vec3<f32>,
436        global_state: &'a GlobalState,
437        location_markers: &'a MapMarkers,
438        voxel_minimap: &'a VoxelMinimap,
439        extra_markers: &'a [super::map::ExtraMarker],
440    ) -> Self {
441        Self {
442            client,
443            imgs,
444            rot_imgs,
445            world_map,
446            fonts,
447            pulse,
448            common: widget::CommonBuilder::default(),
449            ori,
450            global_state,
451            location_markers,
452            voxel_minimap,
453            extra_markers,
454        }
455    }
456}
457
458pub struct State {
459    ids: Ids,
460}
461
462pub enum Event {
463    SettingsChange(InterfaceChange),
464    MoveMiniMap(Vec2<f64>),
465}
466
467impl Widget for MiniMap<'_> {
468    type Event = Vec<Event>;
469    type State = State;
470    type Style = ();
471
472    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
473        State {
474            ids: Ids::new(id_gen),
475        }
476    }
477
478    fn style(&self) -> Self::Style {}
479
480    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
481        common_base::prof_span!("Minimap::update");
482        let mut events = Vec::new();
483
484        let widget::UpdateArgs { state, ui, .. } = args;
485        let colored_player_marker = self
486            .global_state
487            .settings
488            .interface
489            .minimap_colored_player_marker;
490        let mut zoom = self.global_state.settings.interface.minimap_zoom;
491        let mut scale = self.global_state.settings.interface.minimap_scale;
492        if scale <= 0.0 {
493            scale = 1.5;
494        }
495        let show_minimap = self.global_state.settings.interface.minimap_show;
496        let is_facing_north = self.global_state.settings.interface.minimap_face_north;
497        let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
498        let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
499        let orientation = if is_facing_north {
500            Vec3::new(0.0, 1.0, 0.0)
501        } else {
502            self.ori
503        };
504
505        let scaled_map_window_size = Vec2::new(174.0 * scale, 190.0 * scale);
506        let map_size = Vec2::new(170.0 * scale, 170.0 * scale);
507        let minimap_pos = self.global_state.settings.hud_position.minimap;
508
509        if show_minimap {
510            Image::new(self.imgs.mmap_frame)
511                .w_h(scaled_map_window_size.x, scaled_map_window_size.y)
512                .top_right_with_margins_on(ui.window, minimap_pos.y, minimap_pos.x)
513                .color(Some(UI_MAIN))
514                .set(state.ids.mmap_frame, ui);
515
516            Image::new(self.imgs.mmap_frame_2)
517                .w_h(scaled_map_window_size.x, scaled_map_window_size.y)
518                .middle_of(state.ids.mmap_frame)
519                .color(Some(UI_HIGHLIGHT_0))
520                .set(state.ids.mmap_frame_2, ui);
521
522            Rectangle::fill_with([170.0 * scale, 170.0 * scale], color::TRANSPARENT)
523                .mid_top_with_margin_on(state.ids.mmap_frame_2, 18.0 * scale)
524                .set(state.ids.mmap_frame_bg, ui);
525
526            // Map size in chunk coords
527            let worldsize = self.world_map.1;
528            // Map Layers
529            // It is assumed that there is at least one layer
530            if state.ids.map_layers.len() < self.world_map.0.len() {
531                state.update(|state| {
532                    state
533                        .ids
534                        .map_layers
535                        .resize(self.world_map.0.len(), &mut ui.widget_id_generator())
536                });
537            }
538
539            if Button::image(self.imgs.mmap_open)
540                .w_h(18.0 * scale, 18.0 * scale)
541                .hover_image(self.imgs.mmap_open_hover)
542                .press_image(self.imgs.mmap_open_press)
543                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
544                .image_color(UI_HIGHLIGHT_0)
545                .set(state.ids.mmap_button, ui)
546                .was_clicked()
547            {
548                events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
549            }
550
551            // Zoom Buttons
552            // Don't forget to update the code in src/mop/hud.rs:4728 in the
553            // `handle_map_zoom` fn when updating this! TODO: Consolidate
554            // minimap zooming, because having duplicate handlers for hotkey and interface
555            // is error prone. Find the other occurrence by searching for this comment.
556
557            // Pressing + multiplies, and - divides, zoom by ZOOM_FACTOR.
558            const ZOOM_FACTOR: f64 = 2.0;
559
560            // TODO: Either prevent zooming all the way in, *or* see if we can interpolate
561            // somehow if you zoom in too far.  Or both.
562            let min_zoom = 1.0;
563            let max_zoom = worldsize
564                .reduce_partial_max() as f64/*.min(f64::MAX)*/;
565
566            // NOTE: Not sure if a button can be clicked while disabled, but we still double
567            // check for both kinds of zoom to make sure that not only was the
568            // button clicked, it is also okay to perform the zoom action.
569            // Note that since `Button::image` has side effects, we must perform
570            // the `can_zoom_in` and `can_zoom_out` checks after the `&&` to avoid
571            // undesired early termination.
572            let can_zoom_in = zoom < max_zoom;
573            let can_zoom_out = zoom > min_zoom;
574
575            if Button::image(self.imgs.mmap_minus)
576                .w_h(16.0 * scale, 18.0 * scale)
577                .hover_image(self.imgs.mmap_minus_hover)
578                .press_image(self.imgs.mmap_minus_press)
579                .top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
580                .image_color(UI_HIGHLIGHT_0)
581                .enabled(can_zoom_out)
582                .set(state.ids.mmap_minus, ui)
583                .was_clicked()
584                && can_zoom_out
585            {
586                // Set the image dimensions here, rather than recomputing each time.
587                zoom = (zoom / ZOOM_FACTOR).clamp(min_zoom, max_zoom);
588                // set_image_dims(zoom);
589                events.push(Event::SettingsChange(MinimapZoom(zoom)));
590            }
591
592            if Button::image(self.imgs.mmap_plus)
593                .w_h(18.0 * scale, 18.0 * scale)
594                .hover_image(self.imgs.mmap_plus_hover)
595                .press_image(self.imgs.mmap_plus_press)
596                .right_from(state.ids.mmap_minus, 0.0)
597                .image_color(UI_HIGHLIGHT_0)
598                .enabled(can_zoom_in)
599                .set(state.ids.mmap_plus, ui)
600                .was_clicked()
601                && can_zoom_in
602            {
603                zoom = (zoom * ZOOM_FACTOR).clamp(min_zoom, max_zoom);
604                // set_image_dims(zoom);
605                events.push(Event::SettingsChange(MinimapZoom(zoom)));
606            }
607
608            // Always northfacing button
609            if Button::image(if is_facing_north {
610                self.imgs.mmap_north_press
611            } else {
612                self.imgs.mmap_north
613            })
614            .w_h(18.0 * scale, 18.0 * scale)
615            .hover_image(if is_facing_north {
616                self.imgs.mmap_north_press_hover
617            } else {
618                self.imgs.mmap_north_hover
619            })
620            .press_image(if is_facing_north {
621                self.imgs.mmap_north_press_hover
622            } else {
623                self.imgs.mmap_north_press
624            })
625            .left_from(state.ids.mmap_button, 0.0)
626            .image_color(UI_HIGHLIGHT_0)
627            .set(state.ids.mmap_north_button, ui)
628            .was_clicked()
629            {
630                events.push(Event::SettingsChange(MinimapFaceNorth(!is_facing_north)));
631            }
632
633            // Coordinates
634            let player_pos = self
635                .client
636                .state()
637                .ecs()
638                .read_storage::<comp::Pos>()
639                .get(self.client.entity())
640                .map_or(Vec3::zero(), |pos| pos.0);
641
642            // Get map image source rectangle dimensions.
643            let w_src = max_zoom / zoom;
644            let h_src = max_zoom / zoom;
645
646            // Set map image to be centered around player coordinates.
647            let rect_src = position::Rect::from_xy_dim(
648                [
649                    player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64,
650                    worldsize.y as f64
651                        - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64),
652                ],
653                [w_src, h_src],
654            );
655
656            // Map Image
657            // Map Layer Images
658            for (index, layer) in self.world_map.0.iter().enumerate() {
659                let world_map_rotation = if is_facing_north {
660                    layer.none
661                } else {
662                    layer.source_north
663                };
664                if index == 0 {
665                    Image::new(world_map_rotation)
666                        .middle_of(state.ids.mmap_frame_bg)
667                        .w_h(map_size.x, map_size.y)
668                        .parent(state.ids.mmap_frame_bg)
669                        .source_rectangle(rect_src)
670                        .set(state.ids.map_layers[index], ui);
671                } else if show_topo_map {
672                    Image::new(world_map_rotation)
673                        .middle_of(state.ids.mmap_frame_bg)
674                        .w_h(map_size.x, map_size.y)
675                        .parent(state.ids.mmap_frame_bg)
676                        .source_rectangle(rect_src)
677                        .graphics_for(state.ids.map_layers[0])
678                        .set(state.ids.map_layers[index], ui);
679                }
680            }
681            if show_voxel_map {
682                let voxelmap_rotation = if is_facing_north {
683                    self.voxel_minimap.image_id.none
684                } else {
685                    self.voxel_minimap.image_id.source_north
686                };
687                let cmod: Vec2<f64> = player_pos.xy().map2(TerrainChunkSize::RECT_SIZE, |i, j| {
688                    (i as f64).rem_euclid(j as f64)
689                });
690                let rect_src = position::Rect::from_xy_dim(
691                    [
692                        cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
693                        -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
694                    ],
695                    [
696                        TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom,
697                        TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom,
698                    ],
699                );
700                Image::new(voxelmap_rotation)
701                    .middle_of(state.ids.mmap_frame_bg)
702                    .w_h(map_size.x, map_size.y)
703                    .parent(state.ids.mmap_frame_bg)
704                    .source_rectangle(rect_src)
705                    .graphics_for(state.ids.map_layers[0])
706                    .set(state.ids.voxel_minimap, ui);
707            }
708
709            let markers = self
710                .client
711                .markers()
712                .chain(self.extra_markers.iter().map(|em| &em.marker))
713                .collect::<Vec<_>>();
714
715            // Map icons
716            if state.ids.mmap_site_icons.len() < markers.len() {
717                state.update(|state| {
718                    state
719                        .ids
720                        .mmap_site_icons
721                        .resize(markers.len(), &mut ui.widget_id_generator())
722                });
723            }
724            if state.ids.mmap_site_icons_bgs.len() < markers.len() {
725                state.update(|state| {
726                    state
727                        .ids
728                        .mmap_site_icons_bgs
729                        .resize(markers.len(), &mut ui.widget_id_generator())
730                });
731            }
732
733            let wpos_to_rpos = |wpos: Vec2<f32>, limit: bool| {
734                // Site pos in world coordinates relative to the player
735                let rwpos = wpos - player_pos;
736                // Convert to chunk coordinates
737                let rcpos = rwpos.wpos_to_cpos();
738                // Convert to fractional coordinates relative to the worldsize
739                let rfpos = rcpos / max_zoom as f32;
740                // Convert to unrotated pixel coordinates from the player location on the map
741                // (the center)
742                // Accounting for zooming
743                let rpixpos = rfpos.map2(map_size, |e, sz| e * sz as f32 * zoom as f32);
744                let rpos = Vec2::unit_x().rotated_z(orientation.x) * rpixpos.x
745                    + Vec2::unit_y().rotated_z(orientation.x) * rpixpos.y;
746
747                if rpos
748                    .map2(map_size, |e, sz| e.abs() > sz as f32 / 2.0)
749                    .reduce_or()
750                {
751                    limit.then(|| {
752                        let clamped = rpos / rpos.map(|e| e.abs()).reduce_partial_max();
753                        clamped * map_size.map(|e| e as f32) / 2.0
754                    })
755                } else {
756                    Some(rpos)
757                }
758            };
759
760            for (i, marker) in markers.iter().enumerate() {
761                let rpos =
762                    match wpos_to_rpos(marker.wpos, marker.flags.contains(MarkerFlags::IS_QUEST)) {
763                        Some(rpos) => rpos,
764                        None => continue,
765                    };
766                let difficulty = match &marker.kind {
767                    MarkerKind::ChapelSite => Some(4),
768                    MarkerKind::Terracotta => Some(5),
769                    MarkerKind::Gnarling => Some(0),
770                    MarkerKind::Adlet => Some(1),
771                    MarkerKind::Sahagin => Some(2),
772                    MarkerKind::Haniwa => Some(3),
773                    MarkerKind::Cultist => Some(5),
774                    MarkerKind::Myrmidon => Some(4),
775                    MarkerKind::DwarvenMine => Some(5),
776                    MarkerKind::VampireCastle => Some(2),
777                    _ => None,
778                };
779
780                Image::new(match &marker.kind {
781                    MarkerKind::Unknown => self.imgs.mmap_unknown_bg,
782                    MarkerKind::Town => self.imgs.mmap_site_town_bg,
783                    MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel_bg,
784                    MarkerKind::Terracotta => self.imgs.mmap_site_terracotta_bg,
785                    MarkerKind::Castle => self.imgs.mmap_site_castle_bg,
786                    MarkerKind::Cave => self.imgs.mmap_site_cave_bg,
787                    MarkerKind::Tree => self.imgs.mmap_site_tree,
788                    MarkerKind::Gnarling => self.imgs.mmap_site_gnarling_bg,
789                    MarkerKind::Bridge => self.imgs.mmap_site_bridge_bg,
790                    MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course_bg,
791                    MarkerKind::Adlet => self.imgs.mmap_site_adlet_bg,
792                    MarkerKind::Haniwa => self.imgs.mmap_site_haniwa_bg,
793                    MarkerKind::Cultist => self.imgs.mmap_site_cultist_bg,
794                    MarkerKind::Sahagin => self.imgs.mmap_site_sahagin_bg,
795                    MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon_bg,
796                    MarkerKind::DwarvenMine => self.imgs.mmap_site_mine_bg,
797                    MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle_bg,
798                    MarkerKind::Character => self.imgs.mmap_character,
799                })
800                .x_y_position_relative_to(
801                    state.ids.map_layers[0],
802                    position::Relative::Scalar(rpos.x as f64),
803                    position::Relative::Scalar(rpos.y as f64),
804                )
805                .w_h(20.0, 20.0)
806                .color(Some(match difficulty {
807                    Some(0) => QUALITY_LOW,
808                    Some(1) => QUALITY_COMMON,
809                    Some(2) => QUALITY_MODERATE,
810                    Some(3) => QUALITY_HIGH,
811                    Some(4) => QUALITY_EPIC,
812                    Some(5) => QUALITY_DEBUG,
813                    _ => Color::Rgba(1.0, 1.0, 1.0, 0.0),
814                }))
815                .set(state.ids.mmap_site_icons_bgs[i], ui);
816                Image::new(match &marker.kind {
817                    MarkerKind::Unknown => self.imgs.mmap_unknown,
818                    MarkerKind::Town => self.imgs.mmap_site_town,
819                    MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel,
820                    MarkerKind::Terracotta => self.imgs.mmap_site_terracotta,
821                    MarkerKind::Castle => self.imgs.mmap_site_castle,
822                    MarkerKind::Cave => self.imgs.mmap_site_cave,
823                    MarkerKind::Tree => self.imgs.mmap_site_tree,
824                    MarkerKind::Gnarling => self.imgs.mmap_site_gnarling,
825                    MarkerKind::Bridge => self.imgs.mmap_site_bridge,
826                    MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course,
827                    MarkerKind::Adlet => self.imgs.mmap_site_adlet,
828                    MarkerKind::Haniwa => self.imgs.mmap_site_haniwa,
829                    MarkerKind::Cultist => self.imgs.mmap_site_cultist,
830                    MarkerKind::Sahagin => self.imgs.mmap_site_sahagin,
831                    MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon,
832                    MarkerKind::DwarvenMine => self.imgs.mmap_site_mine,
833                    MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle,
834                    MarkerKind::Character => self.imgs.mmap_character,
835                })
836                .middle_of(state.ids.mmap_site_icons_bgs[i])
837                .w_h(20.0, 20.0)
838                .color(Some(
839                    super::map::marker_color(marker, self.pulse).unwrap_or(UI_HIGHLIGHT_0),
840                ))
841                .set(state.ids.mmap_site_icons[i], ui);
842            }
843
844            // Group member indicators
845            let client_state = self.client.state();
846            let member_pos = client_state.ecs().read_storage::<comp::Pos>();
847            let group_members = self
848                .client
849                .group_members()
850                .iter()
851                .filter_map(|(u, r)| match r {
852                    Role::Member => Some(u),
853                    Role::Pet => None,
854                })
855                .collect::<Vec<_>>();
856            let group_size = group_members.len();
857            //let in_group = !group_members.is_empty();
858            let id_maps = client_state
859                .ecs()
860                .read_resource::<common_net::sync::IdMaps>();
861            if state.ids.member_indicators.len() < group_size {
862                state.update(|s| {
863                    s.ids
864                        .member_indicators
865                        .resize(group_size, &mut ui.widget_id_generator())
866                })
867            };
868            for (i, &uid) in group_members.iter().copied().enumerate() {
869                let entity = id_maps.uid_entity(uid);
870                let member_pos = entity.and_then(|entity| member_pos.get(entity));
871
872                if let Some(member_pos) = member_pos {
873                    let rpos = match wpos_to_rpos(member_pos.0.xy(), false) {
874                        Some(rpos) => rpos,
875                        None => continue,
876                    };
877
878                    let factor = 1.2;
879                    let z_comparison = (member_pos.0.z - player_pos.z) as i32;
880                    Button::image(match z_comparison {
881                        10..=i32::MAX => self.imgs.indicator_group_up,
882                        i32::MIN..=-10 => self.imgs.indicator_group_down,
883                        _ => self.imgs.indicator_group,
884                    })
885                    .x_y_position_relative_to(
886                        state.ids.map_layers[0],
887                        position::Relative::Scalar(rpos.x as f64),
888                        position::Relative::Scalar(rpos.y as f64),
889                    )
890                    .w_h(16.0 * factor, 16.0 * factor)
891                    .image_color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
892                    .set(state.ids.member_indicators[i], ui);
893                }
894            }
895
896            // Group location markers
897            if state.ids.location_marker_group.len() < self.location_markers.group.len() {
898                state.update(|s| {
899                    s.ids.location_marker_group.resize(
900                        self.location_markers.group.len(),
901                        &mut ui.widget_id_generator(),
902                    )
903                })
904            };
905            for (i, (&uid, &rpos)) in self.location_markers.group.iter().enumerate() {
906                let lm = rpos.as_();
907                if let Some(rpos) = wpos_to_rpos(lm, true) {
908                    let (image_id, factor) = match self.client.group_info().map(|info| info.1) {
909                        Some(leader) if leader == uid => {
910                            (self.imgs.location_marker_group_leader, 1.2)
911                        },
912                        _ => (self.imgs.location_marker_group, 1.0),
913                    };
914
915                    Image::new(image_id)
916                        .x_y_position_relative_to(
917                            state.ids.map_layers[0],
918                            position::Relative::Scalar(rpos.x as f64),
919                            position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
920                        )
921                        .w_h(16.0 * factor, 16.0 * factor)
922                        .parent(ui.window)
923                        .set(state.ids.location_marker_group[i], ui)
924                }
925            }
926
927            // Location marker
928            if let Some(rpos) = self
929                .location_markers
930                .owned
931                .and_then(|lm| wpos_to_rpos(lm.as_(), true))
932            {
933                let factor = 1.2;
934
935                Image::new(self.imgs.location_marker)
936                    .x_y_position_relative_to(
937                        state.ids.map_layers[0],
938                        position::Relative::Scalar(rpos.x as f64),
939                        position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
940                    )
941                    .w_h(16.0 * factor, 16.0 * factor)
942                    .parent(ui.window)
943                    .set(state.ids.location_marker, ui)
944            }
945
946            // Camera direction
947            let (cam_scale, cam_rotation) = if is_facing_north {
948                // shows camera cone when map is locked north
949                // target north targets the camera direction
950                // none targests the games true north direction (confusing, I know)
951                (1.0, self.rot_imgs.view_mmap.target_north)
952            } else {
953                // hides camera cone when mmap is locked north
954                (0.0, self.rot_imgs.view_mmap.none)
955            };
956            Image::new(cam_rotation)
957                .middle_of(state.ids.map_layers[0])
958                .w_h(100.0 * cam_scale, 100.0 * cam_scale)
959                .color(Some(UI_HIGHLIGHT_0))
960                .parent(ui.window)
961                .set(state.ids.camera, ui);
962
963            // Player indicator/direction
964            let ind_scale = 0.4;
965            let ind_rotation = if colored_player_marker {
966                self.rot_imgs.indicator_mmap_colored.target_player
967            } else {
968                self.rot_imgs.indicator_mmap.target_player
969            };
970            Image::new(ind_rotation)
971                .middle_of(state.ids.map_layers[0])
972                .w_h(32.0 * ind_scale, 37.0 * ind_scale)
973                .color(Some(UI_HIGHLIGHT_0))
974                .parent(ui.window)
975                .set(state.ids.indicator, ui);
976
977            // Compass directions
978            let dirs = [
979                (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true),
980                (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false),
981                (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false),
982                (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false),
983            ];
984            for (dir, id, name, bold) in dirs.iter() {
985                let cardinal_dir = Vec2::unit_x().rotated_z(orientation.x as f64) * dir.x
986                    + Vec2::unit_y().rotated_z(orientation.x as f64) * dir.y;
987                let clamped = cardinal_dir / cardinal_dir.map(|e| e.abs()).reduce_partial_max();
988                let pos = clamped * (map_size / 2.0 - 10.0);
989                Text::new(name)
990                    .x_y_position_relative_to(
991                        state.ids.map_layers[0],
992                        position::Relative::Scalar(pos.x),
993                        position::Relative::Scalar(pos.y),
994                    )
995                    .font_size(self.fonts.cyri.scale(18))
996                    .font_id(self.fonts.cyri.conrod_id)
997                    .color(if *bold {
998                        Color::Rgba(0.75, 0.0, 0.0, 1.0)
999                    } else {
1000                        TEXT_COLOR
1001                    })
1002                    .parent(ui.window)
1003                    .set(*id, ui);
1004            }
1005        } else {
1006            Image::new(self.imgs.mmap_frame_closed)
1007                .w_h(scaled_map_window_size.x, 18.0 * scale)
1008                .color(Some(UI_MAIN))
1009                .top_right_with_margins_on(ui.window, minimap_pos.y, minimap_pos.x)
1010                .set(state.ids.mmap_frame, ui);
1011
1012            if Button::image(self.imgs.mmap_closed)
1013                .w_h(18.0 * scale, 18.0 * scale)
1014                .hover_image(self.imgs.mmap_closed_hover)
1015                .press_image(self.imgs.mmap_closed_press)
1016                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
1017                .image_color(UI_HIGHLIGHT_0)
1018                .set(state.ids.mmap_button, ui)
1019                .was_clicked()
1020            {
1021                events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
1022            }
1023        }
1024
1025        // TODO: Subregion name display
1026
1027        // Title
1028
1029        match self.client.current_chunk() {
1030            Some(chunk) => {
1031                // Count characters in the name to avoid clipping with the name display
1032                if let Some(name) = chunk.meta().name() {
1033                    let name_len = name.chars().count();
1034                    let y_position = if show_minimap {
1035                        (map_size / 2.0 + 3.0).y
1036                    } else {
1037                        3.0
1038                    };
1039                    let text = Text::new(name)
1040                        .align_middle_x_of(state.ids.mmap_frame)
1041                        .y_position_relative_to(
1042                            state.ids.mmap_frame,
1043                            position::Relative::Scalar(y_position),
1044                        )
1045                        .font_size(self.fonts.cyri.scale(match name_len {
1046                            0..=5 => 12 + (4.0 * scale).round() as u32,
1047                            6..=10 => 10 + (4.0 * scale).round() as u32,
1048                            11..=15 => 8 + (4.0 * scale).round() as u32,
1049                            16..=20 => 6 + (4.0 * scale).round() as u32,
1050                            21..=25 => 4 + (4.0 * scale).round() as u32,
1051                            _ => 2 + (4.0 * scale).round() as u32,
1052                        }))
1053                        .font_id(self.fonts.cyri.conrod_id)
1054                        .color(TEXT_COLOR);
1055
1056                    text.set(state.ids.mmap_location, ui);
1057                }
1058            },
1059            None => Text::new(" ")
1060                .mid_top_with_margin_on(state.ids.mmap_frame, 0.0)
1061                .font_size(self.fonts.cyri.scale(18))
1062                .color(TEXT_COLOR)
1063                .set(state.ids.mmap_location, ui),
1064        }
1065
1066        if self
1067            .global_state
1068            .settings
1069            .interface
1070            .toggle_draggable_windows
1071        {
1072            // Create the draggable area & handle move events
1073            let draggable_dim = if show_minimap {
1074                // Subtract 70 for the left and right button widths
1075                // (16 + 18 + 18 + 18)
1076                [scaled_map_window_size.x - (70.0 * scale), 18.0 * scale]
1077            } else {
1078                // View is collapsed, adjust draggable area only based on right button
1079                [scaled_map_window_size.x - (18.0 * scale), 18.0 * scale]
1080            };
1081
1082            // If open, account for the two buttons on the right, if closed, account for the
1083            // one.
1084            let scaled_offset = if show_minimap {
1085                36.0 * scale
1086            } else {
1087                18.0 * scale
1088            };
1089
1090            Rectangle::fill_with(draggable_dim, color::TRANSPARENT)
1091                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, scaled_offset)
1092                .floating(true)
1093                .set(state.ids.draggable_area, ui);
1094
1095            let pos_delta: Vec2<f64> = ui
1096                .widget_input(state.ids.draggable_area)
1097                .drags()
1098                .left()
1099                .map(|drag| Vec2::<f64>::from(drag.delta_xy))
1100                .sum();
1101
1102            // The minimap uses top_right_with_margins_on which means
1103            // we have to use positive margins to move left and positive
1104            // to move down, so we have to invert the (x, y) values.
1105            let pos_delta = [-pos_delta.x, -pos_delta.y];
1106
1107            let window_clamp = Vec2::new(ui.win_w, ui.win_h) - scaled_map_window_size;
1108
1109            let new_pos = (minimap_pos + pos_delta)
1110                .map(|e| e.max(0.))
1111                .map2(window_clamp, |e, bounds| e.min(bounds));
1112
1113            if new_pos.abs_diff_ne(&minimap_pos, f64::EPSILON) {
1114                events.push(Event::MoveMiniMap(new_pos));
1115            }
1116
1117            if ui
1118                .widget_input(state.ids.draggable_area)
1119                .clicks()
1120                .right()
1121                .count()
1122                == 1
1123            {
1124                events.push(Event::MoveMiniMap(HudPositionSettings::default().minimap));
1125            }
1126        }
1127
1128        events
1129    }
1130}