use super::{ img_ids::{Imgs, ImgsRot}, Show, QUALITY_COMMON, QUALITY_DEBUG, QUALITY_EPIC, QUALITY_HIGH, QUALITY_LOW, QUALITY_MODERATE, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::ui::{fonts::Fonts, img_ids}; use client::{self, Client}; use common::{comp, msg::world_msg::SiteKind, terrain::TerrainChunkSize, vol::RectVolSize}; use conrod_core::{ color, position, widget::{self, Button, Image, Rectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; use specs::WorldExt; use vek::*; widget_ids! { struct Ids { mmap_frame, mmap_frame_2, mmap_frame_bg, mmap_location, mmap_button, mmap_plus, mmap_minus, grid, indicator, mmap_north, mmap_east, mmap_south, mmap_west, mmap_site_icons_bgs[], mmap_site_icons[], } } #[derive(WidgetCommon)] pub struct MiniMap<'a> { show: &'a Show, client: &'a Client, imgs: &'a Imgs, rot_imgs: &'a ImgsRot, world_map: &'a (img_ids::Rotations, Vec2), fonts: &'a Fonts, #[conrod(common_builder)] common: widget::CommonBuilder, ori: Vec3, } impl<'a> MiniMap<'a> { pub fn new( show: &'a Show, client: &'a Client, imgs: &'a Imgs, rot_imgs: &'a ImgsRot, world_map: &'a (img_ids::Rotations, Vec2), fonts: &'a Fonts, ori: Vec3, ) -> Self { Self { show, client, imgs, rot_imgs, world_map, fonts, common: widget::CommonBuilder::default(), ori, } } } pub struct State { ids: Ids, zoom: f64, } pub enum Event { Toggle, } impl<'a> Widget for MiniMap<'a> { type Event = Option; type State = State; type Style = (); fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { State { ids: Ids::new(id_gen), zoom: { let min_world_dim = self.world_map.1.reduce_partial_min() as f64; min_world_dim.min( min_world_dim * (TerrainChunkSize::RECT_SIZE.reduce_partial_max() as f64 / 32.0) * (16.0 / 1024.0), ) }, } } #[allow(clippy::unused_unit)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; let zoom = state.zoom; const SCALE: f64 = 1.5; // TODO Make this a setting if self.show.mini_map { Image::new(self.imgs.mmap_frame) .w_h(174.0 * SCALE, 190.0 * SCALE) .top_right_with_margins_on(ui.window, 5.0, 5.0) .color(Some(UI_MAIN)) .set(state.ids.mmap_frame, ui); Image::new(self.imgs.mmap_frame_2) .w_h(174.0 * SCALE, 190.0 * SCALE) .middle_of(state.ids.mmap_frame) .color(Some(UI_HIGHLIGHT_0)) .set(state.ids.mmap_frame_2, ui); Rectangle::fill_with([170.0 * SCALE, 170.0 * SCALE], color::TRANSPARENT) .mid_top_with_margin_on(state.ids.mmap_frame_2, 18.0 * SCALE) .set(state.ids.mmap_frame_bg, ui); // Map size in chunk coords let (world_map, worldsize) = self.world_map; // Zoom Buttons // Pressing + multiplies, and - divides, zoom by ZOOM_FACTOR. const ZOOM_FACTOR: f64 = 2.0; // TODO: Either prevent zooming all the way in, *or* see if we can interpolate // somehow if you zoom in too far. Or both. let min_zoom = 1.0; let max_zoom = worldsize .reduce_partial_max() as f64/*.min(f64::MAX)*/; // NOTE: Not sure if a button can be clicked while disabled, but we still double // check for both kinds of zoom to make sure that not only was the // button clicked, it is also okay to perform the zoom action. // Note that since `Button::image` has side effects, we must perform // the `can_zoom_in` and `can_zoom_out` checks after the `&&` to avoid // undesired early termination. let can_zoom_in = zoom < max_zoom; let can_zoom_out = zoom > min_zoom; if Button::image(self.imgs.mmap_minus) .w_h(16.0 * SCALE, 18.0 * SCALE) .hover_image(self.imgs.mmap_minus_hover) .press_image(self.imgs.mmap_minus_press) .top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0) .image_color(UI_HIGHLIGHT_0) .enabled(can_zoom_out) .set(state.ids.mmap_minus, ui) .was_clicked() && can_zoom_out { // Set the image dimensions here, rather than recomputing each time. let zoom = min_zoom.max(zoom / ZOOM_FACTOR); state.update(|s| s.zoom = zoom); // set_image_dims(zoom); } if Button::image(self.imgs.mmap_plus) .w_h(18.0 * SCALE, 18.0 * SCALE) .hover_image(self.imgs.mmap_plus_hover) .press_image(self.imgs.mmap_plus_press) .right_from(state.ids.mmap_minus, 0.0) .image_color(UI_HIGHLIGHT_0) .enabled(can_zoom_in) .set(state.ids.mmap_plus, ui) .was_clicked() && can_zoom_in { let zoom = max_zoom.min(zoom * ZOOM_FACTOR); state.update(|s| s.zoom = zoom); // set_image_dims(zoom); } // Reload zoom in case it changed. let zoom = state.zoom; // Coordinates let player_pos = self .client .state() .ecs() .read_storage::() .get(self.client.entity()) .map_or(Vec3::zero(), |pos| pos.0); // Get map image source rectangle dimensions. let w_src = max_zoom / zoom; let h_src = max_zoom / zoom; // Set map image to be centered around player coordinates. let rect_src = position::Rect::from_xy_dim( [ player_pos.x as f64 / TerrainChunkSize::RECT_SIZE.x as f64, worldsize.y as f64 - (player_pos.y as f64 / TerrainChunkSize::RECT_SIZE.y as f64), ], [w_src, h_src], ); let map_size = Vec2::new(170.0 * SCALE, 170.0 * SCALE); // Map Image Image::new(world_map.source_north) .middle_of(state.ids.mmap_frame_bg) .w_h(map_size.x, map_size.y) .parent(state.ids.mmap_frame_bg) .source_rectangle(rect_src) .set(state.ids.grid, ui); // Indicator let ind_scale = 0.4; Image::new(self.rot_imgs.indicator_mmap_small.none) .middle_of(state.ids.grid) .w_h(32.0 * ind_scale, 37.0 * ind_scale) .color(Some(UI_HIGHLIGHT_0)) .set(state.ids.indicator, ui); // Map icons if state.ids.mmap_site_icons.len() < self.client.sites().len() { state.update(|state| { state .ids .mmap_site_icons .resize(self.client.sites().len(), &mut ui.widget_id_generator()) }); } if state.ids.mmap_site_icons_bgs.len() < self.client.sites().len() { state.update(|state| { state .ids .mmap_site_icons_bgs .resize(self.client.sites().len(), &mut ui.widget_id_generator()) }); } for (i, site) in self.client.sites().iter().enumerate() { // Site pos in world coordinates relative to the player let rwpos = site.wpos.map(|e| e as f32) - player_pos; // Convert to chunk coordinates let rcpos = rwpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as f32); // Convert to fractional coordinates relative to the worldsize let rfpos = rcpos.map2(*worldsize, |e, sz| e / sz as f32); // Convert to unrotated pixel coordinates from the player location on the map // (the center) // Accounting for zooming let rpixpos = rfpos.map2(map_size, |e, sz| e * sz as f32 * zoom as f32); let rpos = Vec2::unit_x().rotated_z(self.ori.x) * rpixpos.x + Vec2::unit_y().rotated_z(self.ori.x) * rpixpos.y; if rpos .map2(map_size, |e, sz| e.abs() > sz as f32 / 2.0) .reduce_or() { continue; } Image::new(match &site.kind { SiteKind::Town => self.imgs.mmap_site_town_bg, SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon_bg, SiteKind::Castle => self.imgs.mmap_site_castle_bg, }) .x_y_position_relative_to( state.ids.grid, position::Relative::Scalar(rpos.x as f64), position::Relative::Scalar(rpos.y as f64), ) .w_h(20.0, 20.0) .color(Some(match &site.kind { SiteKind::Town => Color::Rgba(1.0, 1.0, 1.0, 0.0), SiteKind::Castle => Color::Rgba(1.0, 1.0, 1.0, 0.0), SiteKind::Dungeon { difficulty } => match difficulty { 0 => QUALITY_LOW, 1 => QUALITY_COMMON, 2 => QUALITY_MODERATE, 3 => QUALITY_HIGH, 4 => QUALITY_EPIC, 5 => QUALITY_DEBUG, _ => Color::Rgba(1.0, 1.0, 1.0, 0.0), }, })) .parent(state.ids.grid) .set(state.ids.mmap_site_icons_bgs[i], ui); Image::new(match &site.kind { SiteKind::Town => self.imgs.mmap_site_town, SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon, SiteKind::Castle => self.imgs.mmap_site_castle, }) .middle_of(state.ids.mmap_site_icons_bgs[i]) .w_h(20.0, 20.0) .color(Some(UI_HIGHLIGHT_0)) .set(state.ids.mmap_site_icons[i], ui); } // Compass directions let dirs = [ (Vec2::new(0.0, 1.0), state.ids.mmap_north, "N", true), (Vec2::new(1.0, 0.0), state.ids.mmap_east, "E", false), (Vec2::new(0.0, -1.0), state.ids.mmap_south, "S", false), (Vec2::new(-1.0, 0.0), state.ids.mmap_west, "W", false), ]; for (dir, id, name, bold) in dirs.iter() { let cardinal_dir = Vec2::unit_x().rotated_z(self.ori.x as f64) * dir.x + Vec2::unit_y().rotated_z(self.ori.x as f64) * dir.y; let clamped = cardinal_dir / cardinal_dir.map(|e| e.abs()).reduce_partial_max(); let pos = clamped * (map_size / 2.0 - 10.0); Text::new(name) .x_y_position_relative_to( state.ids.grid, position::Relative::Scalar(pos.x), position::Relative::Scalar(pos.y), ) .font_size(self.fonts.cyri.scale(18)) .font_id(self.fonts.cyri.conrod_id) .color(if *bold { Color::Rgba(0.75, 0.0, 0.0, 1.0) } else { TEXT_COLOR }) .parent(ui.window) .set(*id, ui); } } else { Image::new(self.imgs.mmap_frame_closed) .w_h(174.0 * SCALE, 18.0 * SCALE) .color(Some(UI_MAIN)) .top_right_with_margins_on(ui.window, 0.0, 5.0) .set(state.ids.mmap_frame, ui); } if Button::image(if self.show.mini_map { self.imgs.mmap_open } else { self.imgs.mmap_closed }) .w_h(18.0 * SCALE, 18.0 * SCALE) .hover_image(if self.show.mini_map { self.imgs.mmap_open_hover } else { self.imgs.mmap_closed_hover }) .press_image(if self.show.mini_map { self.imgs.mmap_open_press } else { self.imgs.mmap_closed_press }) .top_right_with_margins_on(state.ids.mmap_frame, 0.0, 0.0) .image_color(UI_HIGHLIGHT_0) .set(state.ids.mmap_button, ui) .was_clicked() { return Some(Event::Toggle); } // TODO: Subregion name display // Title match self.client.current_chunk() { Some(chunk) => { // Count characters in the name to avoid clipping with the name display let name_len = chunk.meta().name().chars().count(); Text::new(chunk.meta().name()) .mid_top_with_margin_on(state.ids.mmap_frame, match name_len { 15..=30 => 4.0, _ => 2.0, }) .font_size(self.fonts.cyri.scale(match name_len { 0..=15 => 18, 16..=30 => 14, _ => 14, })) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.mmap_location, ui) }, None => Text::new(" ") .mid_top_with_margin_on(state.ids.mmap_frame, 0.0) .font_size(self.fonts.cyri.scale(18)) .color(TEXT_COLOR) .set(state.ids.mmap_location, ui), } None } }