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