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