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 slowjob::SlowJobPool,
18 terrain::{
19 Block, BlockKind, CoordinateConversions, TerrainChunk, TerrainChunkSize, TerrainGrid,
20 },
21 vol::{ReadVol, RectVolSize},
22};
23use common_net::msg::world_msg::{Marker, MarkerKind};
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 {
171 if let Some((_, column)) = self.keyed_jobs.spawn(Some(pool), key, || {
172 let arc_chunk = Arc::clone(chunk);
173 move |_| {
174 let mut layers = Vec::new();
175 const MODE_OVERHEAD: bool = true;
176 if MODE_OVERHEAD {
177 Self::composite_layer_overhead(&arc_chunk, &mut layers);
178 } else {
179 Self::composite_layer_slice(&arc_chunk, &mut layers);
180 }
181 let above = arc_chunk
182 .get(Vec3::new(0, 0, arc_chunk.get_max_z() + 1))
183 .ok()
184 .copied()
185 .unwrap_or_else(Block::empty);
186 let below = arc_chunk
187 .get(Vec3::new(0, 0, arc_chunk.get_min_z() - 1))
188 .ok()
189 .copied()
190 .unwrap_or_else(Block::empty);
191 MinimapColumn {
192 zlo: arc_chunk.get_min_z(),
193 layers,
194 above: (
195 Self::block_color(&above).unwrap_or_else(Rgba::zero),
196 above.is_filled(),
197 ),
198 below: (
199 Self::block_color(&below).unwrap_or_else(Rgba::zero),
200 below.is_filled(),
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 }
211 new_chunks
212 }
213
214 fn add_chunks_to_replace(&mut self, terrain: &TerrainGrid, changes: &TerrainChanges) {
215 changes
216 .modified_blocks
217 .iter()
218 .filter(|(key, old_block)| {
219 terrain
220 .get(**key)
221 .is_ok_and(|new_block| new_block.is_terrain() != old_block.is_terrain())
222 })
223 .map(|(key, _)| terrain.pos_key(*key))
224 .for_each(|key| {
225 self.chunks_to_replace.insert(key);
226 });
227 }
228
229 fn remove_chunks_far(&mut self, terrain: &TerrainGrid, cpos: Vec2<i32>) {
230 let key_predicate = |key: &Vec2<i32>| {
231 let delta: Vec2<u32> = (key - cpos).map(i32::abs).as_();
232 delta.x < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.x
233 && delta.y < 1 + VOXEL_MINIMAP_SIDELENGTH / TerrainChunkSize::RECT_SIZE.y
234 && terrain.get_key(*key).is_some()
235 };
236 self.chunks_to_replace.retain(&key_predicate);
237 self.chunk_minimaps.retain(|key, _| key_predicate(key));
238 }
239
240 pub fn maintain(&mut self, client: &Client, ui: &mut Ui) {
241 let player = client.entity();
242 let pos = if let Some(pos) = client.state().ecs().read_storage::<comp::Pos>().get(player) {
243 pos.0
244 } else {
245 return;
246 };
247 let vpos = pos.xy() - VOXEL_MINIMAP_SIDELENGTH as f32 / 2.0;
248 let cpos: Vec2<i32> = vpos
249 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
250 .as_();
251
252 let pool = client.state().ecs().read_resource::<SlowJobPool>();
253 let terrain = client.state().terrain();
254 let changed_blocks = client.state().terrain_changes();
255 self.add_chunks_to_replace(&terrain, &changed_blocks);
256 let new_chunks = self.add_chunks_near(&pool, &terrain, cpos);
257 self.remove_chunks_far(&terrain, cpos);
258
259 let ceiling_offset = {
265 let voff = Vec2::new(
266 VOXEL_MINIMAP_SIDELENGTH as f32,
267 VOXEL_MINIMAP_SIDELENGTH as f32,
268 ) / 2.0;
269 let coff: Vec2<i32> = voff
270 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
271 .as_();
272 let cmod: Vec2<i32> = vpos
273 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
274 .as_();
275 let column = self.chunk_minimaps.get(&(cpos + coff));
276 #[expect(clippy::unnecessary_lazy_evaluations)]
278 column
279 .map(
280 |MinimapColumn {
281 zlo, layers, above, ..
282 }| {
283 (0..layers.len() as i32)
284 .find(|dz| {
285 layers
286 .get((pos.z as i32 - zlo + dz) as usize)
287 .and_then(|grid| grid.get(cmod)).is_some_and(|(_, b)| *b)
288 })
289 .unwrap_or_else(||
290 if above.1 {
293 1
298 } else {
299 i32::MAX
302 }
303 )
304 },
305 )
306 .unwrap_or(0)
307 };
308 if self.last_pos.xy() != cpos
309 || self.last_pos.z != pos.z as i32
310 || self.last_ceiling != ceiling_offset
311 || new_chunks
312 {
313 self.last_pos = cpos.with_z(pos.z as i32);
314 self.last_ceiling = ceiling_offset;
315 for y in 0..VOXEL_MINIMAP_SIDELENGTH {
316 for x in 0..VOXEL_MINIMAP_SIDELENGTH {
317 let voff = Vec2::new(x as f32, y as f32);
318 let coff: Vec2<i32> = voff
319 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).div_euclid(j))
320 .as_();
321 let cmod: Vec2<i32> = voff
322 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
323 .as_();
324 let column = self.chunk_minimaps.get(&(cpos + coff));
325 let color: Rgba<u8> = column
326 .and_then(|column| {
327 let MinimapColumn {
328 zlo,
329 layers,
330 above,
331 below,
332 } = column;
333 if (pos.z as i32).saturating_add(ceiling_offset) < *zlo {
334 Some(Rgba::new(0, 0, 0, 255))
338 } else {
339 #[expect(clippy::unnecessary_lazy_evaluations)]
345 layers
346 .get(
347 (((pos.z as i32 - zlo).saturating_add(ceiling_offset))
348 as usize)
349 .min(layers.len().saturating_sub(1)),
350 )
351 .and_then(|grid| grid.get(cmod).map(|c| c.0.as_()))
352 .or_else(|| {
353 Some(if pos.z as i32 > *zlo {
354 above.0
355 } else {
356 below.0
357 })
358 })
359 }
360 })
361 .unwrap_or_else(Rgba::zero);
362 self.composited.put_pixel(
363 x,
364 VOXEL_MINIMAP_SIDELENGTH - y - 1,
365 image::Rgba([color.r, color.g, color.b, color.a]),
366 );
367 }
368 }
369
370 ui.replace_graphic(
371 self.image_id.none,
372 Graphic::Image(
373 Arc::new(DynamicImage::ImageRgba8(self.composited.clone())),
374 Some(Rgba::from([0.0, 0.0, 0.0, 0.0])),
375 ),
376 );
377 }
378 }
379}
380
381widget_ids! {
382 struct Ids {
383 mmap_frame,
384 mmap_frame_2,
385 mmap_frame_bg,
386 mmap_location,
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 #[conrod(common_builder)]
414 common: widget::CommonBuilder,
415 ori: Vec3<f32>,
416 global_state: &'a GlobalState,
417 location_markers: &'a MapMarkers,
418 voxel_minimap: &'a VoxelMinimap,
419 extra_markers: &'a HashMap<Vec2<i32>, Marker>,
420}
421
422impl<'a> MiniMap<'a> {
423 pub fn new(
424 client: &'a Client,
425 imgs: &'a Imgs,
426 rot_imgs: &'a ImgsRot,
427 world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
428 fonts: &'a Fonts,
429 ori: Vec3<f32>,
430 global_state: &'a GlobalState,
431 location_markers: &'a MapMarkers,
432 voxel_minimap: &'a VoxelMinimap,
433 extra_markers: &'a HashMap<Vec2<i32>, Marker>,
434 ) -> Self {
435 Self {
436 client,
437 imgs,
438 rot_imgs,
439 world_map,
440 fonts,
441 common: widget::CommonBuilder::default(),
442 ori,
443 global_state,
444 location_markers,
445 voxel_minimap,
446 extra_markers,
447 }
448 }
449}
450
451pub struct State {
452 ids: Ids,
453}
454
455pub enum Event {
456 SettingsChange(InterfaceChange),
457}
458
459impl Widget for MiniMap<'_> {
460 type Event = Vec<Event>;
461 type State = State;
462 type Style = ();
463
464 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
465 State {
466 ids: Ids::new(id_gen),
467 }
468 }
469
470 fn style(&self) -> Self::Style {}
471
472 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
473 common_base::prof_span!("Minimap::update");
474 let mut events = Vec::new();
475
476 let widget::UpdateArgs { state, ui, .. } = args;
477 let mut zoom = self.global_state.settings.interface.minimap_zoom;
478 const SCALE: f64 = 1.5; let show_minimap = self.global_state.settings.interface.minimap_show;
480 let is_facing_north = self.global_state.settings.interface.minimap_face_north;
481 let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
482 let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
483 let orientation = if is_facing_north {
484 Vec3::new(0.0, 1.0, 0.0)
485 } else {
486 self.ori
487 };
488
489 if show_minimap {
490 Image::new(self.imgs.mmap_frame)
491 .w_h(174.0 * SCALE, 190.0 * SCALE)
492 .top_right_with_margins_on(ui.window, 5.0, 5.0)
493 .color(Some(UI_MAIN))
494 .set(state.ids.mmap_frame, ui);
495 Image::new(self.imgs.mmap_frame_2)
496 .w_h(174.0 * SCALE, 190.0 * SCALE)
497 .middle_of(state.ids.mmap_frame)
498 .color(Some(UI_HIGHLIGHT_0))
499 .set(state.ids.mmap_frame_2, ui);
500 Rectangle::fill_with([170.0 * SCALE, 170.0 * SCALE], color::TRANSPARENT)
501 .mid_top_with_margin_on(state.ids.mmap_frame_2, 18.0 * SCALE)
502 .set(state.ids.mmap_frame_bg, ui);
503
504 let worldsize = self.world_map.1;
506 if state.ids.map_layers.len() < self.world_map.0.len() {
509 state.update(|state| {
510 state
511 .ids
512 .map_layers
513 .resize(self.world_map.0.len(), &mut ui.widget_id_generator())
514 });
515 }
516
517 const ZOOM_FACTOR: f64 = 2.0;
525
526 let min_zoom = 1.0;
529 let max_zoom = worldsize
530 .reduce_partial_max() as f64;
531
532 let can_zoom_in = zoom < max_zoom;
539 let can_zoom_out = zoom > min_zoom;
540
541 if Button::image(self.imgs.mmap_minus)
542 .w_h(16.0 * SCALE, 18.0 * SCALE)
543 .hover_image(self.imgs.mmap_minus_hover)
544 .press_image(self.imgs.mmap_minus_press)
545 .top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
546 .image_color(UI_HIGHLIGHT_0)
547 .enabled(can_zoom_out)
548 .set(state.ids.mmap_minus, ui)
549 .was_clicked()
550 && can_zoom_out
551 {
552 zoom = (zoom / ZOOM_FACTOR).clamp(min_zoom, max_zoom);
554 events.push(Event::SettingsChange(MinimapZoom(zoom)));
556 }
557
558 if Button::image(self.imgs.mmap_plus)
559 .w_h(18.0 * SCALE, 18.0 * SCALE)
560 .hover_image(self.imgs.mmap_plus_hover)
561 .press_image(self.imgs.mmap_plus_press)
562 .right_from(state.ids.mmap_minus, 0.0)
563 .image_color(UI_HIGHLIGHT_0)
564 .enabled(can_zoom_in)
565 .set(state.ids.mmap_plus, ui)
566 .was_clicked()
567 && can_zoom_in
568 {
569 zoom = (zoom * ZOOM_FACTOR).clamp(min_zoom, max_zoom);
570 events.push(Event::SettingsChange(MinimapZoom(zoom)));
572 }
573
574 if Button::image(if is_facing_north {
576 self.imgs.mmap_north_press
577 } else {
578 self.imgs.mmap_north
579 })
580 .w_h(18.0 * SCALE, 18.0 * SCALE)
581 .hover_image(if is_facing_north {
582 self.imgs.mmap_north_press_hover
583 } else {
584 self.imgs.mmap_north_hover
585 })
586 .press_image(if is_facing_north {
587 self.imgs.mmap_north_press_hover
588 } else {
589 self.imgs.mmap_north_press
590 })
591 .left_from(state.ids.mmap_button, 0.0)
592 .image_color(UI_HIGHLIGHT_0)
593 .set(state.ids.mmap_north_button, ui)
594 .was_clicked()
595 {
596 events.push(Event::SettingsChange(MinimapFaceNorth(!is_facing_north)));
597 }
598
599 let player_pos = self
601 .client
602 .state()
603 .ecs()
604 .read_storage::<comp::Pos>()
605 .get(self.client.entity())
606 .map_or(Vec3::zero(), |pos| pos.0);
607
608 let w_src = max_zoom / zoom;
610 let h_src = max_zoom / zoom;
611
612 let rect_src = position::Rect::from_xy_dim(
614 [
615 player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64,
616 worldsize.y as f64
617 - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64),
618 ],
619 [w_src, h_src],
620 );
621
622 let map_size = Vec2::new(170.0 * SCALE, 170.0 * SCALE);
623
624 for (index, layer) in self.world_map.0.iter().enumerate() {
627 let world_map_rotation = if is_facing_north {
628 layer.none
629 } else {
630 layer.source_north
631 };
632 if index == 0 {
633 Image::new(world_map_rotation)
634 .middle_of(state.ids.mmap_frame_bg)
635 .w_h(map_size.x, map_size.y)
636 .parent(state.ids.mmap_frame_bg)
637 .source_rectangle(rect_src)
638 .set(state.ids.map_layers[index], ui);
639 } else if show_topo_map {
640 Image::new(world_map_rotation)
641 .middle_of(state.ids.mmap_frame_bg)
642 .w_h(map_size.x, map_size.y)
643 .parent(state.ids.mmap_frame_bg)
644 .source_rectangle(rect_src)
645 .graphics_for(state.ids.map_layers[0])
646 .set(state.ids.map_layers[index], ui);
647 }
648 }
649 if show_voxel_map {
650 let voxelmap_rotation = if is_facing_north {
651 self.voxel_minimap.image_id.none
652 } else {
653 self.voxel_minimap.image_id.source_north
654 };
655 let cmod: Vec2<f64> = player_pos.xy().map2(TerrainChunkSize::RECT_SIZE, |i, j| {
656 (i as f64).rem_euclid(j as f64)
657 });
658 let rect_src = position::Rect::from_xy_dim(
659 [
660 cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
661 -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
662 ],
663 [
664 TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom,
665 TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom,
666 ],
667 );
668 Image::new(voxelmap_rotation)
669 .middle_of(state.ids.mmap_frame_bg)
670 .w_h(map_size.x, map_size.y)
671 .parent(state.ids.mmap_frame_bg)
672 .source_rectangle(rect_src)
673 .graphics_for(state.ids.map_layers[0])
674 .set(state.ids.voxel_minimap, ui);
675 }
676
677 let markers = self
678 .client
679 .markers()
680 .chain(self.extra_markers.values())
681 .collect::<Vec<_>>();
682
683 if state.ids.mmap_site_icons.len() < markers.len() {
685 state.update(|state| {
686 state
687 .ids
688 .mmap_site_icons
689 .resize(markers.len(), &mut ui.widget_id_generator())
690 });
691 }
692 if state.ids.mmap_site_icons_bgs.len() < markers.len() {
693 state.update(|state| {
694 state
695 .ids
696 .mmap_site_icons_bgs
697 .resize(markers.len(), &mut ui.widget_id_generator())
698 });
699 }
700
701 let wpos_to_rpos = |wpos: Vec2<f32>, limit: bool| {
702 let rwpos = wpos - player_pos;
704 let rcpos = rwpos.wpos_to_cpos();
706 let rfpos = rcpos / max_zoom as f32;
708 let rpixpos = rfpos.map2(map_size, |e, sz| e * sz as f32 * zoom as f32);
712 let rpos = Vec2::unit_x().rotated_z(orientation.x) * rpixpos.x
713 + Vec2::unit_y().rotated_z(orientation.x) * rpixpos.y;
714
715 if rpos
716 .map2(map_size, |e, sz| e.abs() > sz as f32 / 2.0)
717 .reduce_or()
718 {
719 limit.then(|| {
720 let clamped = rpos / rpos.map(|e| e.abs()).reduce_partial_max();
721 clamped * map_size.map(|e| e as f32) / 2.0
722 })
723 } else {
724 Some(rpos)
725 }
726 };
727
728 for (i, marker) in markers.iter().enumerate() {
729 let rpos = match wpos_to_rpos(marker.wpos.map(|e| e as f32), false) {
730 Some(rpos) => rpos,
731 None => continue,
732 };
733 let difficulty = match &marker.kind {
734 MarkerKind::Unknown => None,
735 MarkerKind::Town => None,
736 MarkerKind::ChapelSite => Some(4),
737 MarkerKind::Terracotta => Some(5),
738 MarkerKind::Castle => None,
739 MarkerKind::Cave => None,
740 MarkerKind::Tree => None,
741 MarkerKind::Gnarling => Some(0),
742 MarkerKind::Bridge | MarkerKind::GliderCourse => None,
743 MarkerKind::Adlet => Some(1),
744 MarkerKind::Sahagin => Some(2),
745 MarkerKind::Haniwa => Some(3),
746 MarkerKind::Cultist => Some(5),
747 MarkerKind::Myrmidon => Some(4),
748 MarkerKind::DwarvenMine => Some(5),
749 MarkerKind::VampireCastle => Some(2),
750 };
751
752 Image::new(match &marker.kind {
753 MarkerKind::Unknown => self.imgs.mmap_unknown_bg,
754 MarkerKind::Town => self.imgs.mmap_site_town_bg,
755 MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel_bg,
756 MarkerKind::Terracotta => self.imgs.mmap_site_terracotta_bg,
757 MarkerKind::Castle => self.imgs.mmap_site_castle_bg,
758 MarkerKind::Cave => self.imgs.mmap_site_cave_bg,
759 MarkerKind::Tree => self.imgs.mmap_site_tree,
760 MarkerKind::Gnarling => self.imgs.mmap_site_gnarling_bg,
761 MarkerKind::Bridge => self.imgs.mmap_site_bridge_bg,
762 MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course_bg,
763 MarkerKind::Adlet => self.imgs.mmap_site_adlet_bg,
764 MarkerKind::Haniwa => self.imgs.mmap_site_haniwa_bg,
765 MarkerKind::Cultist => self.imgs.mmap_site_cultist_bg,
766 MarkerKind::Sahagin => self.imgs.mmap_site_sahagin_bg,
767 MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon_bg,
768 MarkerKind::DwarvenMine => self.imgs.mmap_site_mine_bg,
769 MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle_bg,
770 })
771 .x_y_position_relative_to(
772 state.ids.map_layers[0],
773 position::Relative::Scalar(rpos.x as f64),
774 position::Relative::Scalar(rpos.y as f64),
775 )
776 .w_h(20.0, 20.0)
777 .color(Some(match difficulty {
778 Some(0) => QUALITY_LOW,
779 Some(1) => QUALITY_COMMON,
780 Some(2) => QUALITY_MODERATE,
781 Some(3) => QUALITY_HIGH,
782 Some(4) => QUALITY_EPIC,
783 Some(5) => QUALITY_DEBUG,
784 _ => Color::Rgba(1.0, 1.0, 1.0, 0.0),
785 }))
786 .set(state.ids.mmap_site_icons_bgs[i], ui);
787 Image::new(match &marker.kind {
788 MarkerKind::Unknown => self.imgs.mmap_unknown,
789 MarkerKind::Town => self.imgs.mmap_site_town,
790 MarkerKind::ChapelSite => self.imgs.mmap_site_sea_chapel,
791 MarkerKind::Terracotta => self.imgs.mmap_site_terracotta,
792 MarkerKind::Castle => self.imgs.mmap_site_castle,
793 MarkerKind::Cave => self.imgs.mmap_site_cave,
794 MarkerKind::Tree => self.imgs.mmap_site_tree,
795 MarkerKind::Gnarling => self.imgs.mmap_site_gnarling,
796 MarkerKind::Bridge => self.imgs.mmap_site_bridge,
797 MarkerKind::GliderCourse => self.imgs.mmap_site_glider_course,
798 MarkerKind::Adlet => self.imgs.mmap_site_adlet,
799 MarkerKind::Haniwa => self.imgs.mmap_site_haniwa,
800 MarkerKind::Cultist => self.imgs.mmap_site_cultist,
801 MarkerKind::Sahagin => self.imgs.mmap_site_sahagin,
802 MarkerKind::Myrmidon => self.imgs.mmap_site_myrmidon,
803 MarkerKind::DwarvenMine => self.imgs.mmap_site_mine,
804 MarkerKind::VampireCastle => self.imgs.mmap_site_vampire_castle,
805 })
806 .middle_of(state.ids.mmap_site_icons_bgs[i])
807 .w_h(20.0, 20.0)
808 .color(Some(UI_HIGHLIGHT_0))
809 .set(state.ids.mmap_site_icons[i], ui);
810 }
811
812 let client_state = self.client.state();
814 let member_pos = client_state.ecs().read_storage::<comp::Pos>();
815 let group_members = self
816 .client
817 .group_members()
818 .iter()
819 .filter_map(|(u, r)| match r {
820 Role::Member => Some(u),
821 Role::Pet => None,
822 })
823 .collect::<Vec<_>>();
824 let group_size = group_members.len();
825 let id_maps = client_state
827 .ecs()
828 .read_resource::<common_net::sync::IdMaps>();
829 if state.ids.member_indicators.len() < group_size {
830 state.update(|s| {
831 s.ids
832 .member_indicators
833 .resize(group_size, &mut ui.widget_id_generator())
834 })
835 };
836 for (i, &uid) in group_members.iter().copied().enumerate() {
837 let entity = id_maps.uid_entity(uid);
838 let member_pos = entity.and_then(|entity| member_pos.get(entity));
839
840 if let Some(member_pos) = member_pos {
841 let rpos = match wpos_to_rpos(member_pos.0.xy(), false) {
842 Some(rpos) => rpos,
843 None => continue,
844 };
845
846 let factor = 1.2;
847 let z_comparison = (member_pos.0.z - player_pos.z) as i32;
848 Button::image(match z_comparison {
849 10..=i32::MAX => self.imgs.indicator_group_up,
850 i32::MIN..=-10 => self.imgs.indicator_group_down,
851 _ => self.imgs.indicator_group,
852 })
853 .x_y_position_relative_to(
854 state.ids.map_layers[0],
855 position::Relative::Scalar(rpos.x as f64),
856 position::Relative::Scalar(rpos.y as f64),
857 )
858 .w_h(16.0 * factor, 16.0 * factor)
859 .image_color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
860 .set(state.ids.member_indicators[i], ui);
861 }
862 }
863
864 if state.ids.location_marker_group.len() < self.location_markers.group.len() {
866 state.update(|s| {
867 s.ids.location_marker_group.resize(
868 self.location_markers.group.len(),
869 &mut ui.widget_id_generator(),
870 )
871 })
872 };
873 for (i, (&uid, &rpos)) in self.location_markers.group.iter().enumerate() {
874 let lm = rpos.as_();
875 if let Some(rpos) = wpos_to_rpos(lm, true) {
876 let (image_id, factor) = match self.client.group_info().map(|info| info.1) {
877 Some(leader) if leader == uid => {
878 (self.imgs.location_marker_group_leader, 1.2)
879 },
880 _ => (self.imgs.location_marker_group, 1.0),
881 };
882
883 Image::new(image_id)
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 + 8.0 * factor),
888 )
889 .w_h(16.0 * factor, 16.0 * factor)
890 .parent(ui.window)
891 .set(state.ids.location_marker_group[i], ui)
892 }
893 }
894
895 if let Some(rpos) = self
897 .location_markers
898 .owned
899 .and_then(|lm| wpos_to_rpos(lm.as_(), true))
900 {
901 let factor = 1.2;
902
903 Image::new(self.imgs.location_marker)
904 .x_y_position_relative_to(
905 state.ids.map_layers[0],
906 position::Relative::Scalar(rpos.x as f64),
907 position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
908 )
909 .w_h(16.0 * factor, 16.0 * factor)
910 .parent(ui.window)
911 .set(state.ids.location_marker, ui)
912 }
913 let ind_scale = 0.4;
915 let ind_rotation = if is_facing_north {
916 self.rot_imgs.indicator_mmap_small.target_north
917 } else {
918 self.rot_imgs.indicator_mmap_small.none
919 };
920 Image::new(ind_rotation)
921 .middle_of(state.ids.map_layers[0])
922 .w_h(32.0 * ind_scale, 37.0 * ind_scale)
923 .color(Some(UI_HIGHLIGHT_0))
924 .parent(ui.window)
925 .set(state.ids.indicator, ui);
926
927 let dirs = [
929 (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true),
930 (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false),
931 (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false),
932 (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false),
933 ];
934 for (dir, id, name, bold) in dirs.iter() {
935 let cardinal_dir = Vec2::unit_x().rotated_z(orientation.x as f64) * dir.x
936 + Vec2::unit_y().rotated_z(orientation.x as f64) * dir.y;
937 let clamped = cardinal_dir / cardinal_dir.map(|e| e.abs()).reduce_partial_max();
938 let pos = clamped * (map_size / 2.0 - 10.0);
939 Text::new(name)
940 .x_y_position_relative_to(
941 state.ids.map_layers[0],
942 position::Relative::Scalar(pos.x),
943 position::Relative::Scalar(pos.y),
944 )
945 .font_size(self.fonts.cyri.scale(18))
946 .font_id(self.fonts.cyri.conrod_id)
947 .color(if *bold {
948 Color::Rgba(0.75, 0.0, 0.0, 1.0)
949 } else {
950 TEXT_COLOR
951 })
952 .parent(ui.window)
953 .set(*id, ui);
954 }
955 } else {
956 Image::new(self.imgs.mmap_frame_closed)
957 .w_h(174.0 * SCALE, 18.0 * SCALE)
958 .color(Some(UI_MAIN))
959 .top_right_with_margins_on(ui.window, 0.0, 5.0)
960 .set(state.ids.mmap_frame, ui);
961 }
962
963 if Button::image(if show_minimap {
964 self.imgs.mmap_open
965 } else {
966 self.imgs.mmap_closed
967 })
968 .w_h(18.0 * SCALE, 18.0 * SCALE)
969 .hover_image(if show_minimap {
970 self.imgs.mmap_open_hover
971 } else {
972 self.imgs.mmap_closed_hover
973 })
974 .press_image(if show_minimap {
975 self.imgs.mmap_open_press
976 } else {
977 self.imgs.mmap_closed_press
978 })
979 .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
980 .image_color(UI_HIGHLIGHT_0)
981 .set(state.ids.mmap_button, ui)
982 .was_clicked()
983 {
984 events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
985 }
986
987 match self.client.current_chunk() {
992 Some(chunk) => {
993 if let Some(name) = chunk.meta().name() {
995 let name_len = name.chars().count();
996 Text::new(name)
997 .mid_top_with_margin_on(state.ids.mmap_frame, match name_len {
998 15..=30 => 4.0,
999 _ => 2.0,
1000 })
1001 .font_size(self.fonts.cyri.scale(match name_len {
1002 0..=15 => 18,
1003 16..=30 => 14,
1004 _ => 14,
1005 }))
1006 .font_id(self.fonts.cyri.conrod_id)
1007 .color(TEXT_COLOR)
1008 .set(state.ids.mmap_location, ui)
1009 }
1010 },
1011 None => Text::new(" ")
1012 .mid_top_with_margin_on(state.ids.mmap_frame, 0.0)
1013 .font_size(self.fonts.cyri.scale(18))
1014 .color(TEXT_COLOR)
1015 .set(state.ids.mmap_location, ui),
1016 }
1017
1018 events
1019 }
1020}