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 zlo: i32,
41 layers: Vec<Grid<(Rgba<u8>, bool)>>,
43 above: (Rgba<u8>, bool),
45 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 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 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 if seen_solids > 12 {
137 break;
138 }
139 }
140 let block = chunk.get(Vec3::new(v.x, v.y, z)).ok();
141 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 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 #[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 above.1 {
294 1
299 } else {
300 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 Some(Rgba::new(0, 0, 0, 255))
339 } else {
340 #[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 let worldsize = self.world_map.1;
527 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 const ZOOM_FACTOR: f64 = 2.0;
558
559 let min_zoom = 1.0;
562 let max_zoom = worldsize
563 .reduce_partial_max() as f64;
564
565 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 zoom = (zoom / ZOOM_FACTOR).clamp(min_zoom, max_zoom);
587 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 events.push(Event::SettingsChange(MinimapZoom(zoom)));
605 }
606
607 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 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 let w_src = max_zoom / zoom;
643 let h_src = max_zoom / zoom;
644
645 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 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 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 let rwpos = wpos - player_pos;
735 let rcpos = rwpos.wpos_to_cpos();
737 let rfpos = rcpos / max_zoom as f32;
739 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 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 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 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 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 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 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 match self.client.current_chunk() {
1017 Some(chunk) => {
1018 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 let draggable_dim = if show_minimap {
1061 [scaled_map_window_size.x - (70.0 * scale), 18.0 * scale]
1064 } else {
1065 [scaled_map_window_size.x - (18.0 * scale), 18.0 * scale]
1067 };
1068
1069 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 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}