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::SiteKind;
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}
420
421impl<'a> MiniMap<'a> {
422 pub fn new(
423 client: &'a Client,
424 imgs: &'a Imgs,
425 rot_imgs: &'a ImgsRot,
426 world_map: &'a (Vec<img_ids::Rotations>, Vec2<u32>),
427 fonts: &'a Fonts,
428 ori: Vec3<f32>,
429 global_state: &'a GlobalState,
430 location_markers: &'a MapMarkers,
431 voxel_minimap: &'a VoxelMinimap,
432 ) -> Self {
433 Self {
434 client,
435 imgs,
436 rot_imgs,
437 world_map,
438 fonts,
439 common: widget::CommonBuilder::default(),
440 ori,
441 global_state,
442 location_markers,
443 voxel_minimap,
444 }
445 }
446}
447
448pub struct State {
449 ids: Ids,
450}
451
452pub enum Event {
453 SettingsChange(InterfaceChange),
454}
455
456impl Widget for MiniMap<'_> {
457 type Event = Vec<Event>;
458 type State = State;
459 type Style = ();
460
461 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
462 State {
463 ids: Ids::new(id_gen),
464 }
465 }
466
467 fn style(&self) -> Self::Style {}
468
469 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
470 common_base::prof_span!("Minimap::update");
471 let mut events = Vec::new();
472
473 let widget::UpdateArgs { state, ui, .. } = args;
474 let mut zoom = self.global_state.settings.interface.minimap_zoom;
475 const SCALE: f64 = 1.5; let show_minimap = self.global_state.settings.interface.minimap_show;
477 let is_facing_north = self.global_state.settings.interface.minimap_face_north;
478 let show_topo_map = self.global_state.settings.interface.map_show_topo_map;
479 let show_voxel_map = self.global_state.settings.interface.map_show_voxel_map;
480 let orientation = if is_facing_north {
481 Vec3::new(0.0, 1.0, 0.0)
482 } else {
483 self.ori
484 };
485
486 if show_minimap {
487 Image::new(self.imgs.mmap_frame)
488 .w_h(174.0 * SCALE, 190.0 * SCALE)
489 .top_right_with_margins_on(ui.window, 5.0, 5.0)
490 .color(Some(UI_MAIN))
491 .set(state.ids.mmap_frame, ui);
492 Image::new(self.imgs.mmap_frame_2)
493 .w_h(174.0 * SCALE, 190.0 * SCALE)
494 .middle_of(state.ids.mmap_frame)
495 .color(Some(UI_HIGHLIGHT_0))
496 .set(state.ids.mmap_frame_2, ui);
497 Rectangle::fill_with([170.0 * SCALE, 170.0 * SCALE], color::TRANSPARENT)
498 .mid_top_with_margin_on(state.ids.mmap_frame_2, 18.0 * SCALE)
499 .set(state.ids.mmap_frame_bg, ui);
500
501 let worldsize = self.world_map.1;
503 if state.ids.map_layers.len() < self.world_map.0.len() {
506 state.update(|state| {
507 state
508 .ids
509 .map_layers
510 .resize(self.world_map.0.len(), &mut ui.widget_id_generator())
511 });
512 }
513
514 const ZOOM_FACTOR: f64 = 2.0;
518
519 let min_zoom = 1.0;
522 let max_zoom = worldsize
523 .reduce_partial_max() as f64;
524
525 let can_zoom_in = zoom < max_zoom;
532 let can_zoom_out = zoom > min_zoom;
533
534 if Button::image(self.imgs.mmap_minus)
535 .w_h(16.0 * SCALE, 18.0 * SCALE)
536 .hover_image(self.imgs.mmap_minus_hover)
537 .press_image(self.imgs.mmap_minus_press)
538 .top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
539 .image_color(UI_HIGHLIGHT_0)
540 .enabled(can_zoom_out)
541 .set(state.ids.mmap_minus, ui)
542 .was_clicked()
543 && can_zoom_out
544 {
545 zoom = min_zoom.max(zoom / ZOOM_FACTOR);
547 events.push(Event::SettingsChange(MinimapZoom(zoom)));
549 }
550 if Button::image(self.imgs.mmap_plus)
551 .w_h(18.0 * SCALE, 18.0 * SCALE)
552 .hover_image(self.imgs.mmap_plus_hover)
553 .press_image(self.imgs.mmap_plus_press)
554 .right_from(state.ids.mmap_minus, 0.0)
555 .image_color(UI_HIGHLIGHT_0)
556 .enabled(can_zoom_in)
557 .set(state.ids.mmap_plus, ui)
558 .was_clicked()
559 && can_zoom_in
560 {
561 zoom = min_zoom.max(zoom * ZOOM_FACTOR);
562 events.push(Event::SettingsChange(MinimapZoom(zoom)));
564 }
565
566 if Button::image(if is_facing_north {
568 self.imgs.mmap_north_press
569 } else {
570 self.imgs.mmap_north
571 })
572 .w_h(18.0 * SCALE, 18.0 * SCALE)
573 .hover_image(if is_facing_north {
574 self.imgs.mmap_north_press_hover
575 } else {
576 self.imgs.mmap_north_hover
577 })
578 .press_image(if is_facing_north {
579 self.imgs.mmap_north_press_hover
580 } else {
581 self.imgs.mmap_north_press
582 })
583 .left_from(state.ids.mmap_button, 0.0)
584 .image_color(UI_HIGHLIGHT_0)
585 .set(state.ids.mmap_north_button, ui)
586 .was_clicked()
587 {
588 events.push(Event::SettingsChange(MinimapFaceNorth(!is_facing_north)));
589 }
590
591 let player_pos = self
593 .client
594 .state()
595 .ecs()
596 .read_storage::<comp::Pos>()
597 .get(self.client.entity())
598 .map_or(Vec3::zero(), |pos| pos.0);
599
600 let w_src = max_zoom / zoom;
602 let h_src = max_zoom / zoom;
603
604 let rect_src = position::Rect::from_xy_dim(
606 [
607 player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64,
608 worldsize.y as f64
609 - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64),
610 ],
611 [w_src, h_src],
612 );
613
614 let map_size = Vec2::new(170.0 * SCALE, 170.0 * SCALE);
615
616 for (index, layer) in self.world_map.0.iter().enumerate() {
619 let world_map_rotation = if is_facing_north {
620 layer.none
621 } else {
622 layer.source_north
623 };
624 if index == 0 {
625 Image::new(world_map_rotation)
626 .middle_of(state.ids.mmap_frame_bg)
627 .w_h(map_size.x, map_size.y)
628 .parent(state.ids.mmap_frame_bg)
629 .source_rectangle(rect_src)
630 .set(state.ids.map_layers[index], ui);
631 } else if show_topo_map {
632 Image::new(world_map_rotation)
633 .middle_of(state.ids.mmap_frame_bg)
634 .w_h(map_size.x, map_size.y)
635 .parent(state.ids.mmap_frame_bg)
636 .source_rectangle(rect_src)
637 .graphics_for(state.ids.map_layers[0])
638 .set(state.ids.map_layers[index], ui);
639 }
640 }
641 if show_voxel_map {
642 let voxelmap_rotation = if is_facing_north {
643 self.voxel_minimap.image_id.none
644 } else {
645 self.voxel_minimap.image_id.source_north
646 };
647 let cmod: Vec2<f64> = player_pos
648 .xy()
649 .map2(TerrainChunkSize::RECT_SIZE, |i, j| (i as u32).rem_euclid(j))
650 .as_();
651 let rect_src = position::Rect::from_xy_dim(
652 [
653 cmod.x + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
654 -cmod.y + VOXEL_MINIMAP_SIDELENGTH as f64 / 2.0,
655 ],
656 [
657 TerrainChunkSize::RECT_SIZE.x as f64 * max_zoom / zoom,
658 TerrainChunkSize::RECT_SIZE.y as f64 * max_zoom / zoom,
659 ],
660 );
661 Image::new(voxelmap_rotation)
662 .middle_of(state.ids.mmap_frame_bg)
663 .w_h(map_size.x, map_size.y)
664 .parent(state.ids.mmap_frame_bg)
665 .source_rectangle(rect_src)
666 .graphics_for(state.ids.map_layers[0])
667 .set(state.ids.voxel_minimap, ui);
668 }
669
670 if state.ids.mmap_site_icons.len() < self.client.sites().len() {
672 state.update(|state| {
673 state
674 .ids
675 .mmap_site_icons
676 .resize(self.client.sites().len(), &mut ui.widget_id_generator())
677 });
678 }
679 if state.ids.mmap_site_icons_bgs.len() < self.client.sites().len() {
680 state.update(|state| {
681 state
682 .ids
683 .mmap_site_icons_bgs
684 .resize(self.client.sites().len(), &mut ui.widget_id_generator())
685 });
686 }
687
688 let wpos_to_rpos = |wpos: Vec2<f32>, limit: bool| {
689 let rwpos = wpos - player_pos;
691 let rcpos = rwpos.wpos_to_cpos();
693 let rfpos = rcpos / max_zoom as f32;
695 let rpixpos = rfpos.map2(map_size, |e, sz| e * sz as f32 * zoom as f32);
699 let rpos = Vec2::unit_x().rotated_z(orientation.x) * rpixpos.x
700 + Vec2::unit_y().rotated_z(orientation.x) * rpixpos.y;
701
702 if rpos
703 .map2(map_size, |e, sz| e.abs() > sz as f32 / 2.0)
704 .reduce_or()
705 {
706 limit.then(|| {
707 let clamped = rpos / rpos.map(|e| e.abs()).reduce_partial_max();
708 clamped * map_size.map(|e| e as f32) / 2.0
709 })
710 } else {
711 Some(rpos)
712 }
713 };
714
715 for (i, site_rich) in self.client.sites().values().enumerate() {
716 let site = &site_rich.site;
717
718 let rpos = match wpos_to_rpos(site.wpos.map(|e| e as f32), false) {
719 Some(rpos) => rpos,
720 None => continue,
721 };
722 let difficulty = match &site.kind {
723 SiteKind::Town => None,
724 SiteKind::ChapelSite => Some(4),
725 SiteKind::Terracotta => Some(5),
726 SiteKind::Castle => None,
727 SiteKind::Cave => None,
728 SiteKind::Tree => None,
729 SiteKind::Gnarling => Some(0),
730 SiteKind::Bridge | SiteKind::GliderCourse => None,
731 SiteKind::Adlet => Some(1),
732 SiteKind::Sahagin => Some(2),
733 SiteKind::Haniwa => Some(3),
734 SiteKind::Cultist => Some(5),
735 SiteKind::Myrmidon => Some(4),
736 SiteKind::DwarvenMine => Some(5),
737 SiteKind::VampireCastle => Some(2),
738 };
739
740 Image::new(match &site.kind {
741 SiteKind::Town => self.imgs.mmap_site_town_bg,
742 SiteKind::ChapelSite => self.imgs.mmap_site_sea_chapel_bg,
743 SiteKind::Terracotta => self.imgs.mmap_site_terracotta_bg,
744 SiteKind::Castle => self.imgs.mmap_site_castle_bg,
745 SiteKind::Cave => self.imgs.mmap_site_cave_bg,
746 SiteKind::Tree => self.imgs.mmap_site_tree,
747 SiteKind::Gnarling => self.imgs.mmap_site_gnarling_bg,
748 SiteKind::Bridge => self.imgs.mmap_site_bridge_bg,
749 SiteKind::GliderCourse => self.imgs.mmap_site_glider_course_bg,
750 SiteKind::Adlet => self.imgs.mmap_site_adlet_bg,
751 SiteKind::Haniwa => self.imgs.mmap_site_haniwa_bg,
752 SiteKind::Cultist => self.imgs.mmap_site_cultist_bg,
753 SiteKind::Sahagin => self.imgs.mmap_site_sahagin_bg,
754 SiteKind::Myrmidon => self.imgs.mmap_site_myrmidon_bg,
755 SiteKind::DwarvenMine => self.imgs.mmap_site_mine_bg,
756 SiteKind::VampireCastle => self.imgs.mmap_site_vampire_castle_bg,
757 })
758 .x_y_position_relative_to(
759 state.ids.map_layers[0],
760 position::Relative::Scalar(rpos.x as f64),
761 position::Relative::Scalar(rpos.y as f64),
762 )
763 .w_h(20.0, 20.0)
764 .color(Some(match difficulty {
765 Some(0) => QUALITY_LOW,
766 Some(1) => QUALITY_COMMON,
767 Some(2) => QUALITY_MODERATE,
768 Some(3) => QUALITY_HIGH,
769 Some(4) => QUALITY_EPIC,
770 Some(5) => QUALITY_DEBUG,
771 _ => Color::Rgba(1.0, 1.0, 1.0, 0.0),
772 }))
773 .set(state.ids.mmap_site_icons_bgs[i], ui);
774 Image::new(match &site.kind {
775 SiteKind::Town => self.imgs.mmap_site_town,
776 SiteKind::ChapelSite => self.imgs.mmap_site_sea_chapel,
777 SiteKind::Terracotta => self.imgs.mmap_site_terracotta,
778 SiteKind::Castle => self.imgs.mmap_site_castle,
779 SiteKind::Cave => self.imgs.mmap_site_cave,
780 SiteKind::Tree => self.imgs.mmap_site_tree,
781 SiteKind::Gnarling => self.imgs.mmap_site_gnarling,
782 SiteKind::Bridge => self.imgs.mmap_site_bridge,
783 SiteKind::GliderCourse => self.imgs.mmap_site_glider_course,
784 SiteKind::Adlet => self.imgs.mmap_site_adlet,
785 SiteKind::Haniwa => self.imgs.mmap_site_haniwa,
786 SiteKind::Cultist => self.imgs.mmap_site_cultist,
787 SiteKind::Sahagin => self.imgs.mmap_site_sahagin,
788 SiteKind::Myrmidon => self.imgs.mmap_site_myrmidon,
789 SiteKind::DwarvenMine => self.imgs.mmap_site_mine,
790 SiteKind::VampireCastle => self.imgs.mmap_site_vampire_castle,
791 })
792 .middle_of(state.ids.mmap_site_icons_bgs[i])
793 .w_h(20.0, 20.0)
794 .color(Some(UI_HIGHLIGHT_0))
795 .set(state.ids.mmap_site_icons[i], ui);
796 }
797
798 let client_state = self.client.state();
800 let member_pos = client_state.ecs().read_storage::<comp::Pos>();
801 let group_members = self
802 .client
803 .group_members()
804 .iter()
805 .filter_map(|(u, r)| match r {
806 Role::Member => Some(u),
807 Role::Pet => None,
808 })
809 .collect::<Vec<_>>();
810 let group_size = group_members.len();
811 let id_maps = client_state
813 .ecs()
814 .read_resource::<common_net::sync::IdMaps>();
815 if state.ids.member_indicators.len() < group_size {
816 state.update(|s| {
817 s.ids
818 .member_indicators
819 .resize(group_size, &mut ui.widget_id_generator())
820 })
821 };
822 for (i, &uid) in group_members.iter().copied().enumerate() {
823 let entity = id_maps.uid_entity(uid);
824 let member_pos = entity.and_then(|entity| member_pos.get(entity));
825
826 if let Some(member_pos) = member_pos {
827 let rpos = match wpos_to_rpos(member_pos.0.xy(), false) {
828 Some(rpos) => rpos,
829 None => continue,
830 };
831
832 let factor = 1.2;
833 let z_comparison = (member_pos.0.z - player_pos.z) as i32;
834 Button::image(match z_comparison {
835 10..=i32::MAX => self.imgs.indicator_group_up,
836 i32::MIN..=-10 => self.imgs.indicator_group_down,
837 _ => self.imgs.indicator_group,
838 })
839 .x_y_position_relative_to(
840 state.ids.map_layers[0],
841 position::Relative::Scalar(rpos.x as f64),
842 position::Relative::Scalar(rpos.y as f64),
843 )
844 .w_h(16.0 * factor, 16.0 * factor)
845 .image_color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
846 .set(state.ids.member_indicators[i], ui);
847 }
848 }
849
850 if state.ids.location_marker_group.len() < self.location_markers.group.len() {
852 state.update(|s| {
853 s.ids.location_marker_group.resize(
854 self.location_markers.group.len(),
855 &mut ui.widget_id_generator(),
856 )
857 })
858 };
859 for (i, (&uid, &rpos)) in self.location_markers.group.iter().enumerate() {
860 let lm = rpos.as_();
861 if let Some(rpos) = wpos_to_rpos(lm, true) {
862 let (image_id, factor) = match self.client.group_info().map(|info| info.1) {
863 Some(leader) if leader == uid => {
864 (self.imgs.location_marker_group_leader, 1.2)
865 },
866 _ => (self.imgs.location_marker_group, 1.0),
867 };
868
869 Image::new(image_id)
870 .x_y_position_relative_to(
871 state.ids.map_layers[0],
872 position::Relative::Scalar(rpos.x as f64),
873 position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
874 )
875 .w_h(16.0 * factor, 16.0 * factor)
876 .parent(ui.window)
877 .set(state.ids.location_marker_group[i], ui)
878 }
879 }
880
881 if let Some(rpos) = self
883 .location_markers
884 .owned
885 .and_then(|lm| wpos_to_rpos(lm.as_(), true))
886 {
887 let factor = 1.2;
888
889 Image::new(self.imgs.location_marker)
890 .x_y_position_relative_to(
891 state.ids.map_layers[0],
892 position::Relative::Scalar(rpos.x as f64),
893 position::Relative::Scalar(rpos.y as f64 + 8.0 * factor),
894 )
895 .w_h(16.0 * factor, 16.0 * factor)
896 .parent(ui.window)
897 .set(state.ids.location_marker, ui)
898 }
899 let ind_scale = 0.4;
901 let ind_rotation = if is_facing_north {
902 self.rot_imgs.indicator_mmap_small.target_north
903 } else {
904 self.rot_imgs.indicator_mmap_small.none
905 };
906 Image::new(ind_rotation)
907 .middle_of(state.ids.map_layers[0])
908 .w_h(32.0 * ind_scale, 37.0 * ind_scale)
909 .color(Some(UI_HIGHLIGHT_0))
910 .parent(ui.window)
911 .set(state.ids.indicator, ui);
912
913 let dirs = [
915 (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true),
916 (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false),
917 (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false),
918 (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false),
919 ];
920 for (dir, id, name, bold) in dirs.iter() {
921 let cardinal_dir = Vec2::unit_x().rotated_z(orientation.x as f64) * dir.x
922 + Vec2::unit_y().rotated_z(orientation.x as f64) * dir.y;
923 let clamped = cardinal_dir / cardinal_dir.map(|e| e.abs()).reduce_partial_max();
924 let pos = clamped * (map_size / 2.0 - 10.0);
925 Text::new(name)
926 .x_y_position_relative_to(
927 state.ids.map_layers[0],
928 position::Relative::Scalar(pos.x),
929 position::Relative::Scalar(pos.y),
930 )
931 .font_size(self.fonts.cyri.scale(18))
932 .font_id(self.fonts.cyri.conrod_id)
933 .color(if *bold {
934 Color::Rgba(0.75, 0.0, 0.0, 1.0)
935 } else {
936 TEXT_COLOR
937 })
938 .parent(ui.window)
939 .set(*id, ui);
940 }
941 } else {
942 Image::new(self.imgs.mmap_frame_closed)
943 .w_h(174.0 * SCALE, 18.0 * SCALE)
944 .color(Some(UI_MAIN))
945 .top_right_with_margins_on(ui.window, 0.0, 5.0)
946 .set(state.ids.mmap_frame, ui);
947 }
948
949 if Button::image(if show_minimap {
950 self.imgs.mmap_open
951 } else {
952 self.imgs.mmap_closed
953 })
954 .w_h(18.0 * SCALE, 18.0 * SCALE)
955 .hover_image(if show_minimap {
956 self.imgs.mmap_open_hover
957 } else {
958 self.imgs.mmap_closed_hover
959 })
960 .press_image(if show_minimap {
961 self.imgs.mmap_open_press
962 } else {
963 self.imgs.mmap_closed_press
964 })
965 .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
966 .image_color(UI_HIGHLIGHT_0)
967 .set(state.ids.mmap_button, ui)
968 .was_clicked()
969 {
970 events.push(Event::SettingsChange(MinimapShow(!show_minimap)));
971 }
972
973 match self.client.current_chunk() {
978 Some(chunk) => {
979 if let Some(name) = chunk.meta().name() {
981 let name_len = name.chars().count();
982 Text::new(name)
983 .mid_top_with_margin_on(state.ids.mmap_frame, match name_len {
984 15..=30 => 4.0,
985 _ => 2.0,
986 })
987 .font_size(self.fonts.cyri.scale(match name_len {
988 0..=15 => 18,
989 16..=30 => 14,
990 _ => 14,
991 }))
992 .font_id(self.fonts.cyri.conrod_id)
993 .color(TEXT_COLOR)
994 .set(state.ids.mmap_location, ui)
995 }
996 },
997 None => Text::new(" ")
998 .mid_top_with_margin_on(state.ids.mmap_frame, 0.0)
999 .font_size(self.fonts.cyri.scale(18))
1000 .color(TEXT_COLOR)
1001 .set(state.ids.mmap_location, ui),
1002 }
1003
1004 events
1005 }
1006}