diff --git a/CHANGELOG.md b/CHANGELOG.md index de56e6a875..eb8f325d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dungeons now have multiple kinds of stairs. - Trades now display item prices in tooltips. - Admin designated build areas +- Indicator text to collectable terrain sprites ### Changed diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 542993dedd..e86661dd85 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -597,6 +597,9 @@ impl Item { SpriteKind::MediumGrass => "common.items.grasses.medium", SpriteKind::ShortGrass => "common.items.grasses.short", SpriteKind::Coconut => "common.items.food.coconut", + + // Containers + // IMPORTANT: Add any new container to `SpriteKind::is_container` SpriteKind::Chest => { chosen = Lottery::::load_expect(match rng.gen_range(0..7) { 0 => "common.loot_tables.loot_table_weapon_uncommon", @@ -637,6 +640,7 @@ impl Item { .read(); chosen.choose() }, + SpriteKind::Beehive => "common.items.crafting_ing.honey", SpriteKind::Stones => "common.items.crafting_ing.stones", SpriteKind::Twigs => "common.items.crafting_ing.twigs", diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 56c1857d55..14658221f2 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -240,6 +240,14 @@ impl SpriteKind { } } + /// Is the sprite a container that will emit a mystery item? + pub fn is_container(&self) -> bool { + matches!( + self, + SpriteKind::Chest | SpriteKind::ChestBurried | SpriteKind::Mud | SpriteKind::Crate, + ) + } + pub fn mine_tool(&self) -> Option { match self { SpriteKind::Velorite diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index bdf815d855..00a71bb512 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -53,6 +53,7 @@ use crate::{ i18n::{LanguageMetadata, Localization}, render::{Consts, Globals, RenderMode, Renderer}, scene::camera::{self, Camera}, + session::Interactable, settings::Fps, ui::{ fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, ScaleMode, Ui, @@ -67,7 +68,7 @@ use common::{ self, item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality}, skills::{Skill, SkillGroupKind}, - BuffKind, + BuffKind, Item, }, outcome::Outcome, terrain::TerrainChunk, @@ -901,6 +902,7 @@ impl Hud { dt: Duration, info: HudInfo, camera: &Camera, + interactable: Option, ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = std::mem::replace(&mut self.events, Vec::new()); @@ -1330,19 +1332,7 @@ impl Hud { let mut sct_walker = self.ids.scts.walk(); let mut sct_bg_walker = self.ids.sct_bgs.walk(); - // Render overitem: name, etc. - for (pos, item, distance) in (&entities, &pos, &items) - .join() - .map(|(_, pos, item)| (pos, item, pos.0.distance_squared(player_pos))) - .filter(|(_, _, distance)| distance < &common::consts::MAX_PICKUP_RANGE.powi(2)) - { - let overitem_id = overitem_walker.next( - &mut self.ids.overitems, - &mut ui_widgets.widget_id_generator(), - ); - - let ingame_pos = pos.0 + Vec3::unit_z() * 1.2; - + let make_overitem = |item: &Item, pos, distance, active, fonts| { let text = if item.amount() > 1 { format!("{} x {}", item.amount(), item.name()) } else { @@ -1353,17 +1343,74 @@ impl Hud { // Item overitem::Overitem::new( - &text, - &quality, - &distance, - &self.fonts, + text.into(), + quality, + distance, + fonts, &global_state.settings.controls, + // If we're currently set to interact with the item... + active, ) .x_y(0.0, 100.0) - .position_ingame(ingame_pos) + .position_ingame(pos) + }; + + // Render overitem: name, etc. + for (entity, pos, item, distance) in (&entities, &pos, &items) + .join() + .map(|(entity, pos, item)| (entity, pos, item, pos.0.distance_squared(player_pos))) + .filter(|(_, _, _, distance)| distance < &common::consts::MAX_PICKUP_RANGE.powi(2)) + { + let overitem_id = overitem_walker.next( + &mut self.ids.overitems, + &mut ui_widgets.widget_id_generator(), + ); + + make_overitem( + item, + pos.0 + Vec3::unit_z() * 1.2, + distance, + interactable.as_ref().and_then(|i| i.entity()) == Some(entity), + &self.fonts, + ) .set(overitem_id, ui_widgets); } + // Render overtime for an interactable block + if let Some(Interactable::Block(block, pos)) = interactable { + let overitem_id = overitem_walker.next( + &mut self.ids.overitems, + &mut ui_widgets.widget_id_generator(), + ); + + let pos = pos.map(|e| e as f32 + 0.5); + let over_pos = pos + Vec3::unit_z() * 0.7; + + // This is only done once per frame, so it's not a performance issue + if block.get_sprite().map_or(false, |s| s.is_container()) { + overitem::Overitem::new( + "???".into(), + overitem::TEXT_COLOR, + pos.distance_squared(player_pos), + &self.fonts, + &global_state.settings.controls, + true, + ) + .x_y(0.0, 100.0) + .position_ingame(over_pos) + .set(overitem_id, ui_widgets); + } else if let Some(item) = Item::try_reclaim_from_block(block) { + make_overitem( + &item, + over_pos, + pos.distance_squared(player_pos), + true, + &self.fonts, + ) + .set(overitem_id, ui_widgets); + } + } + let speech_bubbles = &self.speech_bubbles; // Render overhead name tags and health bars @@ -3244,6 +3291,7 @@ impl Hud { camera: &Camera, dt: Duration, info: HudInfo, + interactable: Option, ) -> Vec { span!(_guard, "maintain", "Hud::maintain"); // conrod eats tabs. Un-eat a tabstop so tab completion can work @@ -3272,7 +3320,15 @@ impl Hud { if let Some(maybe_id) = self.to_focus.take() { self.ui.focus_widget(maybe_id); } - let events = self.update_layout(client, global_state, debug_info, dt, info, camera); + let events = self.update_layout( + client, + global_state, + debug_info, + dt, + info, + camera, + interactable, + ); let camera::Dependents { view_mat, proj_mat, .. } = camera.dependents(); diff --git a/voxygen/src/hud/overitem.rs b/voxygen/src/hud/overitem.rs index c46ebc0c85..86f3a13927 100644 --- a/voxygen/src/hud/overitem.rs +++ b/voxygen/src/hud/overitem.rs @@ -7,6 +7,9 @@ use conrod_core::{ widget::{self, RoundedRectangle, Text}, widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon, }; +use std::borrow::Cow; + +pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0); widget_ids! { struct Ids { @@ -23,22 +26,24 @@ widget_ids! { /// (Item, DistanceFromPlayer, Rarity, etc.) #[derive(WidgetCommon)] pub struct Overitem<'a> { - name: &'a str, - quality: &'a Color, - distance_from_player_sqr: &'a f32, + name: Cow<'a, str>, + quality: Color, + distance_from_player_sqr: f32, fonts: &'a Fonts, controls: &'a ControlSettings, #[conrod(common_builder)] common: widget::CommonBuilder, + active: bool, } impl<'a> Overitem<'a> { pub fn new( - name: &'a str, - quality: &'a Color, - distance_from_player_sqr: &'a f32, + name: Cow<'a, str>, + quality: Color, + distance_from_player_sqr: f32, fonts: &'a Fonts, controls: &'a ControlSettings, + active: bool, ) -> Self { Self { name, @@ -47,6 +52,7 @@ impl<'a> Overitem<'a> { fonts, controls, common: widget::CommonBuilder::default(), + active, } } } @@ -84,7 +90,6 @@ impl<'a> Widget for Overitem<'a> { fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { id, state, ui, .. } = args; - let text_color = Color::Rgba(0.61, 0.61, 0.89, 1.0); let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.4); // Example: @@ -121,14 +126,18 @@ impl<'a> Widget for Overitem<'a> { Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) .font_size(text_font_size as u32) - .color(*self.quality) + .color(self.quality) .x_y(0.0, text_pos_y) .depth(self.distance_from_player_sqr + 3.0) .parent(id) .set(state.ids.name, ui); // Pickup Button - if let Some(key_button) = self.controls.get_binding(GameInput::Interact) { + if let Some(key_button) = self + .controls + .get_binding(GameInput::Interact) + .filter(|_| self.active) + { RoundedRectangle::fill_with([btn_rect_size, btn_rect_size], btn_radius, btn_color) .x_y(0.0, btn_rect_pos_y) .depth(self.distance_from_player_sqr + 1.0) @@ -137,7 +146,7 @@ impl<'a> Widget for Overitem<'a> { Text::new(&format!("{}", key_button)) .font_id(self.fonts.cyri.conrod_id) .font_size(btn_font_size as u32) - .color(text_color) + .color(TEXT_COLOR) .x_y(0.0, btn_text_pos_y) .depth(self.distance_from_player_sqr + 2.0) .parent(id) diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 8a5a66be2d..4ec8d92ad8 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -877,6 +877,7 @@ impl PlayState for SessionState { target_entity: self.target_entity, selected_entity: self.selected_entity, }, + self.interactable, ); // Look for changes in the localization files @@ -1678,11 +1679,12 @@ fn under_cursor( &ecs.entities(), &positions, scales.maybe(), - &ecs.read_storage::() + &ecs.read_storage::(), + ecs.read_storage::().maybe(), ) .join() - .filter(|(e, _, _, _)| *e != player_entity) - .map(|(e, p, s, b)| { + .filter(|(e, _, _, _, _)| *e != player_entity) + .filter_map(|(e, p, s, b, i)| { const RADIUS_SCALE: f32 = 3.0; // TODO: use collider radius instead of body radius? let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; @@ -1690,7 +1692,12 @@ fn under_cursor( let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); // Distance squared from camera to the entity let dist_sqr = pos.distance_squared(cam_pos); - (e, pos, radius, dist_sqr) + // We only care about interacting with entities that contain items, or are not inanimate (to trade with) + if i.is_some() || !matches!(b, comp::Body::Object(_)) { + Some((e, pos, radius, dist_sqr)) + } else { + None + } }) // Roughly filter out entities farther than ray distance .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2)) @@ -1730,13 +1737,13 @@ fn under_cursor( } #[derive(Clone, Copy)] -enum Interactable { +pub enum Interactable { Block(Block, Vec3), Entity(specs::Entity), } impl Interactable { - fn entity(self) -> Option { + pub fn entity(self) -> Option { match self { Self::Entity(e) => Some(e), Self::Block(_, _) => None,