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        indicator,
395        mmap_north,
396        mmap_east,
397        mmap_south,
398        mmap_west,
399        mmap_site_icons_bgs[],
400        mmap_site_icons[],
401        member_indicators[],
402        location_marker,
403        location_marker_group[],
404        voxel_minimap,
405        draggable_area,
406    }
407}
408
409#[derive(WidgetCommon)]
410pub struct MiniMap<'a> {
411    client: &'a Client,
412    imgs: &'a Imgs,
413    rot_imgs: &'a ImgsRot,
414    world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
415    fonts: &'a Fonts,
416    pulse: f32,
417    #[conrod(common_builder)]
418    common: widget::CommonBuilder,
419    ori: Vec3<f32>,
420    global_state: &'a GlobalState,
421    location_markers: &'a MapMarkers,
422    voxel_minimap: &'a VoxelMinimap,
423    extra_markers: &'a [super::map::ExtraMarker],
424}
425
426impl<'a> MiniMap<'a> {
427    pub fn new(
428        client: &'a Client,
429        imgs: &'a Imgs,
430        rot_imgs: &'a ImgsRot,
431        world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
432        fonts: &'a Fonts,
433        pulse: f32,
434        ori: Vec3<f32>,
435        global_state: &'a GlobalState,
436        location_markers: &'a MapMarkers,
437        voxel_minimap: &'a VoxelMinimap,
438        extra_markers: &'a [super::map::ExtraMarker],
439    ) -> Self {
440        Self {
441            client,
442            imgs,
443            rot_imgs,
444            world_map,
445            fonts,
446            pulse,
447            common: widget::CommonBuilder::default(),
448            ori,
449            global_state,
450            location_markers,
451            voxel_minimap,
452            extra_markers,
453        }
454    }
455}
456
457pub struct State {
458    ids: Ids,
459}
460
461pub enum Event {
462    SettingsChange(InterfaceChange),
463    MoveMiniMap(Vec2<f64>),
464}
465
466impl Widget for MiniMap<'_> {
467    type Event = Vec<Event>;
468    type State = State;
469    type Style = ();
470
471    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
472        State {
473            ids: Ids::new(id_gen),
474        }
475    }
476
477    fn style(&self) -> Self::Style {}
478
479    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
480        common_base::prof_span!("Minimap::update");
481        let mut events = Vec::new();
482
483        let widget::UpdateArgs { state, ui, .. } = args;
484        let colored_player_marker = self
485            .global_state
486            .settings
487            .interface
488            .minimap_colored_player_marker;
489        let mut zoom = self.global_state.settings.interface.minimap_zoom;
490        let mut scale = self.global_state.settings.interface.minimap_scale;
491        if scale <= 0.0 {
492            scale = 1.5;
493        }
494        let show_minimap = self.global_state.settings.interface.minimap_show;
495        let is_facing_north = self.global_state.settings.interface.minimap_face_north;
496        let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
497        let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
498        let orientation = if is_facing_north {
499            Vec3::new(0.0, 1.0, 0.0)
500        } else {
501            self.ori
502        };
503
504        let scaled_map_window_size = Vec2::new(174.0 * scale, 190.0 * scale);
505        let map_size = Vec2::new(170.0 * scale, 170.0 * scale);
506        let minimap_pos = self.global_state.settings.hud_position.minimap;
507
508        if show_minimap {
509            Image::new(self.imgs.mmap_frame)
510                .w_h(scaled_map_window_size.x, scaled_map_window_size.y)
511                .top_right_with_margins_on(ui.window, minimap_pos.y, minimap_pos.x)
512                .color(Some(UI_MAIN))
513                .set(state.ids.mmap_frame, ui);
514
515            Image::new(self.imgs.mmap_frame_2)
516                .w_h(scaled_map_window_size.x, scaled_map_window_size.y)
517                .middle_of(state.ids.mmap_frame)
518                .color(Some(UI_HIGHLIGHT_0))
519                .set(state.ids.mmap_frame_2, ui);
520
521            Rectangle::fill_with([170.0 * scale, 170.0 * scale], color::TRANSPARENT)
522                .mid_top_with_margin_on(state.ids.mmap_frame_2, 18.0 * scale)
523                .set(state.ids.mmap_frame_bg, ui);
524
525            // Map size in chunk coords
526            let worldsize = self.world_map.1;
527            // Map Layers
528            // It is assumed that there is at least one layer
529            if state.ids.map_layers.len() < self.world_map.0.len() {
530                state.update(|state| {
531                    state
532                        .ids
533                        .map_layers
534                        .resize(self.world_map.0.len(), &mut ui.widget_id_generator())
535                });
536            }
537
538            if Button::image(self.imgs.mmap_open)
539                .w_h(18.0 * scale, 18.0 * scale)
540                .hover_image(self.imgs.mmap_open_hover)
541                .press_image(self.imgs.mmap_open_press)
542                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
543                .image_color(UI_HIGHLIGHT_0)
544                .set(state.ids.mmap_button, ui)
545                .was_clicked()
546            {
547                events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
548            }
549
550            // Zoom Buttons
551            // Don't forget to update the code in src/mop/hud.rs:4728 in the
552            // `handle_map_zoom` fn when updating this! TODO: Consolidate
553            // minimap zooming, because having duplicate handlers for hotkey and interface
554            // is error prone. Find the other occurrence by searching for this comment.
555
556            // Pressing + multiplies, and - divides, zoom by ZOOM_FACTOR.
557            const ZOOM_FACTOR: f64 = 2.0;
558
559            // TODO: Either prevent zooming all the way in, *or* see if we can interpolate
560            // somehow if you zoom in too far.  Or both.
561            let min_zoom = 1.0;
562            let max_zoom = worldsize
563                .reduce_partial_max() as f64/*.min(f64::MAX)*/;
564
565            // NOTE: Not sure if a button can be clicked while disabled, but we still double
566            // check for both kinds of zoom to make sure that not only was the
567            // button clicked, it is also okay to perform the zoom action.
568            // Note that since `Button::image` has side effects, we must perform
569            // the `can_zoom_in` and `can_zoom_out` checks after the `&&` to avoid
570            // undesired early termination.
571            let can_zoom_in = zoom < max_zoom;
572            let can_zoom_out = zoom > min_zoom;
573
574            if Button::image(self.imgs.mmap_minus)
575                .w_h(16.0 * scale, 18.0 * scale)
576                .hover_image(self.imgs.mmap_minus_hover)
577                .press_image(self.imgs.mmap_minus_press)
578                .top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
579                .image_color(UI_HIGHLIGHT_0)
580                .enabled(can_zoom_out)
581                .set(state.ids.mmap_minus, ui)
582                .was_clicked()
583                && can_zoom_out
584            {
585                // Set the image dimensions here, rather than recomputing each time.
586                zoom = (zoom / ZOOM_FACTOR).clamp(min_zoom, max_zoom);
587                // set_image_dims(zoom);
588                events.push(Event::SettingsChange(MinimapZoom(zoom)));
589            }
590
591            if Button::image(self.imgs.mmap_plus)
592                .w_h(18.0 * scale, 18.0 * scale)
593                .hover_image(self.imgs.mmap_plus_hover)
594                .press_image(self.imgs.mmap_plus_press)
595                .right_from(state.ids.mmap_minus, 0.0)
596                .image_color(UI_HIGHLIGHT_0)
597                .enabled(can_zoom_in)
598                .set(state.ids.mmap_plus, ui)
599                .was_clicked()
600                && can_zoom_in
601            {
602                zoom = (zoom * ZOOM_FACTOR).clamp(min_zoom, max_zoom);
603                // set_image_dims(zoom);
604                events.push(Event::SettingsChange(MinimapZoom(zoom)));
605            }
606
607            // Always northfacing button
608            if Button::image(if is_facing_north {
609                self.imgs.mmap_north_press
610            } else {
611                self.imgs.mmap_north
612            })
613            .w_h(18.0 * scale, 18.0 * scale)
614            .hover_image(if is_facing_north {
615                self.imgs.mmap_north_press_hover
616            } else {
617                self.imgs.mmap_north_hover
618            })
619            .press_image(if is_facing_north {
620                self.imgs.mmap_north_press_hover
621            } else {
622                self.imgs.mmap_north_press
623            })
624            .left_from(state.ids.mmap_button, 0.0)
625            .image_color(UI_HIGHLIGHT_0)
626            .set(state.ids.mmap_north_button, ui)
627            .was_clicked()
628            {
629                events.push(Event::SettingsChange(MinimapFaceNorth(!is_facing_north)));
630            }
631
632            // Coordinates
633            let player_pos = self
634                .client
635                .state()
636                .ecs()
637                .read_storage::<comp::Pos>()
638                .get(self.client.entity())
639                .map_or(Vec3::zero(), |pos| pos.0);
640
641            // Get map image source rectangle dimensions.
642            let w_src = max_zoom / zoom;
643            let h_src = max_zoom / zoom;
644
645            // Set map image to be centered around player coordinates.
646            let rect_src = position::Rect::from_xy_dim(
647                [
648                    player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64,
649                    worldsize.y as f64
650                        - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64),
651                ],
652                [w_src, h_src],
653            );
654
655            // Map Image
656            // Map Layer Images
657            for (index, layer) in self.world_map.0.iter().enumerate() {
658                let world_map_rotation = if is_facing_north {
659                    layer.none
660                } else {
661                    layer.source_north
662                };
663                if index == 0 {
664                    Image::new(world_map_rotation)
665                        .middle_of(state.ids.mmap_frame_bg)
666                        .w_h(map_size.x, map_size.y)
667                        .parent(state.ids.mmap_frame_bg)
668                        .source_rectangle(rect_src)
669                        .set(state.ids.map_layers[index], ui);
670                } else if show_topo_map {
671                    Image::new(world_map_rotation)
672                        .middle_of(state.ids.mmap_frame_bg)
673                        .w_h(map_size.x, map_size.y)
674                        .parent(state.ids.mmap_frame_bg)
675                        .source_rectangle(rect_src)
676                        .graphics_for(state.ids.map_layers[0])
677                        .set(state.ids.map_layers[index], ui);
678                }
679            }
680            if show_voxel_map {
681                let voxelmap_rotation = if is_facing_north {
682                    self.voxel_minimap.image_id.none
683                } else {
684                    self.voxel_minimap.image_id.source_north
685                };
686                let cmod: Vec2<f64> = player_pos.xy().map2(TerrainChunkSize::RECT_SIZE, |i, j| {
687                    (i as f64).rem_euclid(j as f64)
688                });
689                let rect_src = position::Rect::from_xy_dim(
690                    [
691                        cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
692                        -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
693                    ],
694                    [
695                        TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom,
696                        TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom,
697                    ],
698                );
699                Image::new(voxelmap_rotation)
700                    .middle_of(state.ids.mmap_frame_bg)
701                    .w_h(map_size.x, map_size.y)
702                    .parent(state.ids.mmap_frame_bg)
703                    .source_rectangle(rect_src)
704                    .graphics_for(state.ids.map_layers[0])
705                    .set(state.ids.voxel_minimap, ui);
706            }
707
708            let markers = self
709                .client
710                .markers()
711                .chain(self.extra_markers.iter().map(|em| &em.marker))
712                .collect::<Vec<_>>();
713
714            // Map icons
715            if state.ids.mmap_site_icons.len() < markers.len() {
716                state.update(|state| {
717                    state
718                        .ids
719                        .mmap_site_icons
720                        .resize(markers.len(), &mut ui.widget_id_generator())
721                });
722            }
723            if state.ids.mmap_site_icons_bgs.len() < markers.len() {
724                state.update(|state| {
725                    state
726                        .ids
727                        .mmap_site_icons_bgs
728                        .resize(markers.len(), &mut ui.widget_id_generator())
729                });
730            }
731
732            let wpos_to_rpos = |wpos: Vec2<f32>, limit: bool| {
733                // Site pos in world coordinates relative to the player
734                let rwpos = wpos - player_pos;
735                // Convert to chunk coordinates
736                let rcpos = rwpos.wpos_to_cpos();
737                // Convert to fractional coordinates relative to the worldsize
738                let rfpos = rcpos / max_zoom as f32;
739                // Convert to unrotated pixel coordinates from the player location on the map
740                // (the center)
741                // Accounting for zooming
742                let rpixpos = rfpos.map2(map_size, |e, sz| e * sz as f32 * zoom as f32);
743                let rpos = Vec2::unit_x().rotated_z(orientation.x) * rpixpos.x
744                    + Vec2::unit_y().rotated_z(orientation.x) * rpixpos.y;
745
746                if rpos
747                    .map2(map_size, |e, sz| e.abs() > sz as f32 / 2.0)
748                    .reduce_or()
749                {
750                    limit.then(|| {
751                        let clamped = rpos / rpos.map(|e| e.abs()).reduce_partial_max();
752                        clamped * map_size.map(|e| e as f32) / 2.0
753                    })
754                } else {
755                    Some(rpos)
756                }
757            };
758
759            for (i, marker) in markers.iter().enumerate() {
760                let rpos =
761                    match wpos_to_rpos(marker.wpos, marker.flags.contains(MarkerFlags::IS_QUEST)) {
762                        Some(rpos) => rpos,
763                        None => continue,
764                    };
765                let difficulty = match &marker.kind {
766                    MarkerKind::ChapelSite => Some(4),
767                    MarkerKind::Terracotta => Some(5),
768                    MarkerKind::Gnarling => Some(0),
769                    MarkerKind::Adlet => Some(1),
770                    MarkerKind::Sahagin => Some(2),
771                    MarkerKind::Haniwa => Some(3),
772                    MarkerKind::Cultist => Some(5),
773                    MarkerKind::Myrmidon => Some(4),
774                    MarkerKind::DwarvenMine => Some(5),
775                    MarkerKind::VampireCastle => Some(2),
776                    _ => None,
777                };
778
779                Image::new(match &marker.kind {
780                    MarkerKind::Unknown => self.imgs.mmap_unknown_bg,
781                    MarkerKind::Town => self.imgs.mmap_site_town_bg,
782                    MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel_bg,
783                    MarkerKind::Terracotta => self.imgs.mmap_site_terracotta_bg,
784                    MarkerKind::Castle => self.imgs.mmap_site_castle_bg,
785                    MarkerKind::Cave => self.imgs.mmap_site_cave_bg,
786                    MarkerKind::Tree => self.imgs.mmap_site_tree,
787                    MarkerKind::Gnarling => self.imgs.mmap_site_gnarling_bg,
788                    MarkerKind::Bridge => self.imgs.mmap_site_bridge_bg,
789                    MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course_bg,
790                    MarkerKind::Adlet => self.imgs.mmap_site_adlet_bg,
791                    MarkerKind::Haniwa => self.imgs.mmap_site_haniwa_bg,
792                    MarkerKind::Cultist => self.imgs.mmap_site_cultist_bg,
793                    MarkerKind::Sahagin => self.imgs.mmap_site_sahagin_bg,
794                    MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon_bg,
795                    MarkerKind::DwarvenMine => self.imgs.mmap_site_mine_bg,
796                    MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle_bg,
797                    MarkerKind::Character => self.imgs.mmap_character,
798                })
799                .x_y_position_relative_to(
800                    state.ids.map_layers[0],
801                    position::Relative::Scalar(rpos.x as f64),
802                    position::Relative::Scalar(rpos.y as f64),
803                )
804                .w_h(20.0, 20.0)
805                .color(Some(match difficulty {
806                    Some(0) => QUALITY_LOW,
807                    Some(1) => QUALITY_COMMON,
808                    Some(2) => QUALITY_MODERATE,
809                    Some(3) => QUALITY_HIGH,
810                    Some(4) => QUALITY_EPIC,
811                    Some(5) => QUALITY_DEBUG,
812                    _ => Color::Rgba(1.0, 1.0, 1.0, 0.0),
813                }))
814                .set(state.ids.mmap_site_icons_bgs[i], ui);
815                Image::new(match &marker.kind {
816                    MarkerKind::Unknown => self.imgs.mmap_unknown,
817                    MarkerKind::Town => self.imgs.mmap_site_town,
818                    MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel,
819                    MarkerKind::Terracotta => self.imgs.mmap_site_terracotta,
820                    MarkerKind::Castle => self.imgs.mmap_site_castle,
821                    MarkerKind::Cave => self.imgs.mmap_site_cave,
822                    MarkerKind::Tree => self.imgs.mmap_site_tree,
823                    MarkerKind::Gnarling => self.imgs.mmap_site_gnarling,
824                    MarkerKind::Bridge => self.imgs.mmap_site_bridge,
825                    MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course,
826                    MarkerKind::Adlet => self.imgs.mmap_site_adlet,
827                    MarkerKind::Haniwa => self.imgs.mmap_site_haniwa,
828                    MarkerKind::Cultist => self.imgs.mmap_site_cultist,
829                    MarkerKind::Sahagin => self.imgs.mmap_site_sahagin,
830                    MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon,
831                    MarkerKind::DwarvenMine => self.imgs.mmap_site_mine,
832                    MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle,
833                    MarkerKind::Character => self.imgs.mmap_character,
834                })
835                .middle_of(state.ids.mmap_site_icons_bgs[i])
836                .w_h(20.0, 20.0)
837                .color(Some(
838                    super::map::marker_color(marker, self.pulse).unwrap_or(UI_HIGHLIGHT_0),
839                ))
840                .set(state.ids.mmap_site_icons[i], ui);
841            }
842
843            // Group member indicators
844            let client_state = self.client.state();
845            let member_pos = client_state.ecs().read_storage::<comp::Pos>();
846            let group_members = self
847                .client
848                .group_members()
849                .iter()
850                .filter_map(|(u, r)| match r {
851                    Role::Member => Some(u),
852                    Role::Pet => None,
853                })
854                .collect::<Vec<_>>();
855            let group_size = group_members.len();
856            //let in_group = !group_members.is_empty();
857            let id_maps = client_state
858                .ecs()
859                .read_resource::<common_net::sync::IdMaps>();
860            if state.ids.member_indicators.len() < group_size {
861                state.update(|s| {
862                    s.ids
863                        .member_indicators
864                        .resize(group_size, &mut ui.widget_id_generator())
865                })
866            };
867            for (i, &uid) in group_members.iter().copied().enumerate() {
868                let entity = id_maps.uid_entity(uid);
869                let member_pos = entity.and_then(|entity| member_pos.get(entity));
870
871                if let Some(member_pos) = member_pos {
872                    let rpos = match wpos_to_rpos(member_pos.0.xy(), false) {
873                        Some(rpos) => rpos,
874                        None => continue,
875                    };
876
877                    let factor = 1.2;
878                    let z_comparison = (member_pos.0.z - player_pos.z) as i32;
879                    Button::image(match z_comparison {
880                        10..=i32::MAX => self.imgs.indicator_group_up,
881                        i32::MIN..=-10 => self.imgs.indicator_group_down,
882                        _ => self.imgs.indicator_group,
883                    })
884                    .x_y_position_relative_to(
885                        state.ids.map_layers[0],
886                        position::Relative::Scalar(rpos.x as f64),
887                        position::Relative::Scalar(rpos.y as f64),
888                    )
889                    .w_h(16.0 * factor, 16.0 * factor)
890                    .image_color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
891                    .set(state.ids.member_indicators[i], ui);
892                }
893            }
894
895            // Group location markers
896            if state.ids.location_marker_group.len() < self.location_markers.group.len() {
897                state.update(|s| {
898                    s.ids.location_marker_group.resize(
899                        self.location_markers.group.len(),
900                        &mut ui.widget_id_generator(),
901                    )
902                })
903            };
904            for (i, (&uid, &rpos)) in self.location_markers.group.iter().enumerate() {
905                let lm = rpos.as_();
906                if let Some(rpos) = wpos_to_rpos(lm, true) {
907                    let (image_id, factor) = match self.client.group_info().map(|info| info.1) {
908                        Some(leader) if leader == uid => {
909                            (self.imgs.location_marker_group_leader, 1.2)
910                        },
911                        _ => (self.imgs.location_marker_group, 1.0),
912                    };
913
914                    Image::new(image_id)
915                        .x_y_position_relative_to(
916                            state.ids.map_layers[0],
917                            position::Relative::Scalar(rpos.x as f64),
918                            position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
919                        )
920                        .w_h(16.0 * factor, 16.0 * factor)
921                        .parent(ui.window)
922                        .set(state.ids.location_marker_group[i], ui)
923                }
924            }
925
926            // Location marker
927            if let Some(rpos) = self
928                .location_markers
929                .owned
930                .and_then(|lm| wpos_to_rpos(lm.as_(), true))
931            {
932                let factor = 1.2;
933
934                Image::new(self.imgs.location_marker)
935                    .x_y_position_relative_to(
936                        state.ids.map_layers[0],
937                        position::Relative::Scalar(rpos.x as f64),
938                        position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
939                    )
940                    .w_h(16.0 * factor, 16.0 * factor)
941                    .parent(ui.window)
942                    .set(state.ids.location_marker, ui)
943            }
944            // Indicator
945            let ind_scale = 0.4;
946            let ind_rotation = if is_facing_north {
947                if colored_player_marker {
948                    self.rot_imgs.indicator_mmap_colored.target_north
949                } else {
950                    self.rot_imgs.indicator_mmap.target_north
951                }
952            } else if colored_player_marker {
953                self.rot_imgs.indicator_mmap_colored.none
954            } else {
955                self.rot_imgs.indicator_mmap.none
956            };
957            Image::new(ind_rotation)
958                .middle_of(state.ids.map_layers[0])
959                .w_h(32.0 * ind_scale, 37.0 * ind_scale)
960                .color(Some(UI_HIGHLIGHT_0))
961                .parent(ui.window)
962                .set(state.ids.indicator, ui);
963
964            // Compass directions
965            let dirs = [
966                (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true),
967                (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false),
968                (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false),
969                (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false),
970            ];
971            for (dir, id, name, bold) in dirs.iter() {
972                let cardinal_dir = Vec2::unit_x().rotated_z(orientation.x as f64) * dir.x
973                    + Vec2::unit_y().rotated_z(orientation.x as f64) * dir.y;
974                let clamped = cardinal_dir / cardinal_dir.map(|e| e.abs()).reduce_partial_max();
975                let pos = clamped * (map_size / 2.0 - 10.0);
976                Text::new(name)
977                    .x_y_position_relative_to(
978                        state.ids.map_layers[0],
979                        position::Relative::Scalar(pos.x),
980                        position::Relative::Scalar(pos.y),
981                    )
982                    .font_size(self.fonts.cyri.scale(18))
983                    .font_id(self.fonts.cyri.conrod_id)
984                    .color(if *bold {
985                        Color::Rgba(0.75, 0.0, 0.0, 1.0)
986                    } else {
987                        TEXT_COLOR
988                    })
989                    .parent(ui.window)
990                    .set(*id, ui);
991            }
992        } else {
993            Image::new(self.imgs.mmap_frame_closed)
994                .w_h(scaled_map_window_size.x, 18.0 * scale)
995                .color(Some(UI_MAIN))
996                .top_right_with_margins_on(ui.window, minimap_pos.y, minimap_pos.x)
997                .set(state.ids.mmap_frame, ui);
998
999            if Button::image(self.imgs.mmap_closed)
1000                .w_h(18.0 * scale, 18.0 * scale)
1001                .hover_image(self.imgs.mmap_closed_hover)
1002                .press_image(self.imgs.mmap_closed_press)
1003                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
1004                .image_color(UI_HIGHLIGHT_0)
1005                .set(state.ids.mmap_button, ui)
1006                .was_clicked()
1007            {
1008                events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
1009            }
1010        }
1011
1012        // TODO: Subregion name display
1013
1014        // Title
1015
1016        match self.client.current_chunk() {
1017            Some(chunk) => {
1018                // Count characters in the name to avoid clipping with the name display
1019                if let Some(name) = chunk.meta().name() {
1020                    let name_len = name.chars().count();
1021                    let y_position = if show_minimap {
1022                        (map_size / 2.0 + 3.0).y
1023                    } else {
1024                        3.0
1025                    };
1026                    let text = Text::new(name)
1027                        .align_middle_x_of(state.ids.mmap_frame)
1028                        .y_position_relative_to(
1029                            state.ids.mmap_frame,
1030                            position::Relative::Scalar(y_position),
1031                        )
1032                        .font_size(self.fonts.cyri.scale(match name_len {
1033                            0..=5 => 12 + (4.0 * scale).round() as u32,
1034                            6..=10 => 10 + (4.0 * scale).round() as u32,
1035                            11..=15 => 8 + (4.0 * scale).round() as u32,
1036                            16..=20 => 6 + (4.0 * scale).round() as u32,
1037                            21..=25 => 4 + (4.0 * scale).round() as u32,
1038                            _ => 2 + (4.0 * scale).round() as u32,
1039                        }))
1040                        .font_id(self.fonts.cyri.conrod_id)
1041                        .color(TEXT_COLOR);
1042
1043                    text.set(state.ids.mmap_location, ui);
1044                }
1045            },
1046            None => Text::new(" ")
1047                .mid_top_with_margin_on(state.ids.mmap_frame, 0.0)
1048                .font_size(self.fonts.cyri.scale(18))
1049                .color(TEXT_COLOR)
1050                .set(state.ids.mmap_location, ui),
1051        }
1052
1053        if self
1054            .global_state
1055            .settings
1056            .interface
1057            .toggle_draggable_windows
1058        {
1059            // Create the draggable area & handle move events
1060            let draggable_dim = if show_minimap {
1061                // Subtract 70 for the left and right button widths
1062                // (16 + 18 + 18 + 18)
1063                [scaled_map_window_size.x - (70.0 * scale), 18.0 * scale]
1064            } else {
1065                // View is collapsed, adjust draggable area only based on right button
1066                [scaled_map_window_size.x - (18.0 * scale), 18.0 * scale]
1067            };
1068
1069            // If open, account for the two buttons on the right, if closed, account for the
1070            // one.
1071            let scaled_offset = if show_minimap {
1072                36.0 * scale
1073            } else {
1074                18.0 * scale
1075            };
1076
1077            Rectangle::fill_with(draggable_dim, color::TRANSPARENT)
1078                .top_right_with_margins_on(state.ids.mmap_frame, 0.0, scaled_offset)
1079                .floating(true)
1080                .set(state.ids.draggable_area, ui);
1081
1082            let pos_delta: Vec2<f64> = ui
1083                .widget_input(state.ids.draggable_area)
1084                .drags()
1085                .left()
1086                .map(|drag| Vec2::<f64>::from(drag.delta_xy))
1087                .sum();
1088
1089            // The minimap uses top_right_with_margins_on which means
1090            // we have to use positive margins to move left and positive
1091            // to move down, so we have to invert the (x, y) values.
1092            let pos_delta = [-pos_delta.x, -pos_delta.y];
1093
1094            let window_clamp = Vec2::new(ui.win_w, ui.win_h) - scaled_map_window_size;
1095
1096            let new_pos = (minimap_pos + pos_delta)
1097                .map(|e| e.max(0.))
1098                .map2(window_clamp, |e, bounds| e.min(bounds));
1099
1100            if new_pos.abs_diff_ne(&minimap_pos, f64::EPSILON) {
1101                events.push(Event::MoveMiniMap(new_pos));
1102            }
1103
1104            if ui
1105                .widget_input(state.ids.draggable_area)
1106                .clicks()
1107                .right()
1108                .count()
1109                == 1
1110            {
1111                events.push(Event::MoveMiniMap(HudPositionSettings::default().minimap));
1112            }
1113        }
1114
1115        events
1116    }
1117}