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 zlo: i32,
39 layers: Vec<Grid<(Rgba<u8>, bool)>>,
41 above: (Rgba<u8>, bool),
43 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 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 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 if seen_solids > 12 {
135 break;
136 }
137 }
138 let block = chunk.get(Vec3::new(v.x, v.y, z)).ok();
139 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 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 #[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 above.1 {
292 1
297 } else {
298 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 Some(Rgba::new(0, 0, 0, 255))
337 } else {
338 #[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 let worldsize = self.world_map.1;
518 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 const ZOOM_FACTOR: f64 = 2.0;
537
538 let min_zoom = 1.0;
541 let max_zoom = worldsize
542 .reduce_partial_max() as f64;
543
544 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 zoom = (zoom / ZOOM_FACTOR).clamp(min_zoom, max_zoom);
566 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 events.push(Event::SettingsChange(MinimapZoom(zoom)));
584 }
585
586 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 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 let w_src = max_zoom / zoom;
622 let h_src = max_zoom / zoom;
623
624 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 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 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 let rwpos = wpos - player_pos;
714 let rcpos = rwpos.wpos_to_cpos();
716 let rfpos = rcpos / max_zoom as f32;
718 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 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 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 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 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 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 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 match self.client.current_chunk() {
1008 Some(chunk) => {
1009 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}