1use std::{cmp::Reverse, collections::HashSet};
2
3use specs::{Join, LendJoin, ReadStorage, WorldExt};
4use vek::*;
5
6use super::target::{self, Target};
7use client::Client;
8use common::{
9 CachedSpatialGrid,
10 comp::{
11 self, Alignment, Collider, Content, pet, ship::figuredata::VOXEL_COLLIDER_MANIFEST,
12 tool::ToolKind,
13 },
14 consts::{
15 self, MAX_INTERACT_RANGE, MAX_PICKUP_RANGE, MAX_SPRITE_MOUNT_RANGE, TELEPORTER_RADIUS,
16 },
17 link::Is,
18 mounting::{Mount, Volume, VolumePos, VolumeRider},
19 states::utils::can_perform_pet,
20 terrain::{Block, TerrainGrid, UnlockKind},
21 uid::{IdMaps, Uid},
22};
23use common_base::span;
24use hashbrown::HashMap;
25
26use crate::{
27 game_input::GameInput,
28 hud::CraftingTab,
29 scene::{Scene, terrain::Interaction},
30};
31
32#[derive(Debug, Default)]
33pub struct Interactables {
34 pub input_map: HashMap<GameInput, (f32, Interactable)>,
35 pub entities: HashSet<specs::Entity>,
38}
39
40#[derive(Clone, Debug)]
41pub enum BlockInteraction {
42 Collect { steal: bool },
43 Unlock(UnlockKind),
44 Craft(CraftingTab),
45 Mine(ToolKind),
48 Mount,
49 Read(Content),
50 LightToggle(bool),
51}
52
53#[derive(Debug, Clone)]
54pub enum Interactable {
55 Block {
56 block: Block,
57 volume_pos: VolumePos,
58 interaction: BlockInteraction,
59 },
60 Entity {
61 entity: specs::Entity,
62 interaction: EntityInteraction,
63 },
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, enum_map::Enum)]
68pub enum EntityInteraction {
69 HelpDowned,
70 PickupItem,
71 ActivatePortal,
72 Pet,
73 Talk,
74 CampfireSit,
75 Trade,
76 StayFollow,
77 Mount,
78}
79
80impl BlockInteraction {
81 fn from_block_pos(
82 terrain: &TerrainGrid,
83 id_maps: &IdMaps,
84 colliders: &ReadStorage<Collider>,
85 volume_pos: VolumePos,
86 interaction: Interaction,
87 ) -> Option<(Block, Self)> {
88 let block = volume_pos.get_block(terrain, id_maps, colliders)?;
89 let block_interaction = match interaction {
90 Interaction::Collect => {
91 let unlock = match volume_pos.kind {
93 Volume::Terrain => block.get_sprite().and_then(|sprite| {
94 let chunk = terrain.pos_chunk(volume_pos.pos)?;
95 let sprite_chunk_pos = TerrainGrid::chunk_offs(volume_pos.pos);
96 let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
97 let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
98 if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
104 Some(unlock_condition)
105 } else {
106 None
107 }
108 }),
109 Volume::Entity(_) => None,
110 };
111
112 if let Some(unlock) = unlock {
113 BlockInteraction::Unlock(unlock)
114 } else if let Some(mine_tool) = block.mine_tool() {
115 BlockInteraction::Mine(mine_tool)
116 } else {
117 BlockInteraction::Collect {
118 steal: block.is_owned(),
119 }
120 }
121 },
122 Interaction::Read => match volume_pos.kind {
123 common::mounting::Volume::Terrain => block.get_sprite().and_then(|sprite| {
124 let chunk = terrain.pos_chunk(volume_pos.pos)?;
125 let sprite_chunk_pos = TerrainGrid::chunk_offs(volume_pos.pos);
126 let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
127 sprite
128 .content(sprite_cfg.cloned())
129 .map(BlockInteraction::Read)
130 })?,
131 common::mounting::Volume::Entity(_) => return None,
133 },
134 Interaction::Craft(tab) => BlockInteraction::Craft(tab),
135 Interaction::Mount => BlockInteraction::Mount,
136 Interaction::LightToggle(enable) => BlockInteraction::LightToggle(enable),
137 };
138
139 Some((block, block_interaction))
140 }
141
142 pub fn game_input(&self) -> GameInput {
143 match self {
144 BlockInteraction::Collect { .. }
145 | BlockInteraction::Read(_)
146 | BlockInteraction::LightToggle(_)
147 | BlockInteraction::Craft(_)
148 | BlockInteraction::Unlock(_) => GameInput::Interact,
149 BlockInteraction::Mine(_) => GameInput::Primary,
150 BlockInteraction::Mount => GameInput::Mount,
151 }
152 }
153
154 pub fn range(&self) -> f32 {
155 match self {
156 BlockInteraction::Collect { .. }
157 | BlockInteraction::Unlock(_)
158 | BlockInteraction::Mine(_)
159 | BlockInteraction::Craft(_) => consts::MAX_PICKUP_RANGE,
160 BlockInteraction::Mount => consts::MAX_MOUNT_RANGE,
161 BlockInteraction::LightToggle(_) | BlockInteraction::Read(_) => {
162 consts::MAX_INTERACT_RANGE
163 },
164 }
165 }
166}
167
168#[derive(Debug)]
169pub enum GetInteractablesError {
170 ClientMissingPosition,
171 ClientMissingUid,
172}
173
174pub(super) fn get_interactables(
177 client: &Client,
178 collect_target: Option<Target<target::Collectable>>,
179 entity_target: Option<Target<target::Entity>>,
180 mine_target: Option<Target<target::Mine>>,
181 scene: &Scene,
182) -> Result<HashMap<GameInput, (f32, Interactable)>, GetInteractablesError> {
183 span!(_guard, "select_interactable");
184 use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
185
186 let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
187
188 let ecs = client.state().ecs();
189 let id_maps = ecs.read_resource::<IdMaps>();
190 let scene_terrain = scene.terrain();
191 let terrain = client.state().terrain();
192 let player_entity = client.entity();
193 let positions = ecs.read_storage::<comp::Pos>();
194 let player_pos = positions
195 .get(player_entity)
196 .ok_or(GetInteractablesError::ClientMissingPosition)?
197 .0;
198
199 let uids = ecs.read_storage::<Uid>();
200 let healths = ecs.read_storage::<comp::Health>();
201 let colliders = ecs.read_storage::<comp::Collider>();
202 let char_states = ecs.read_storage::<comp::CharacterState>();
203 let is_mounts = ecs.read_storage::<Is<Mount>>();
204 let bodies = ecs.read_storage::<comp::Body>();
205 let masses = ecs.read_storage::<comp::Mass>();
206 let items = ecs.read_storage::<comp::PickupItem>();
207 let alignments = ecs.read_storage::<comp::Alignment>();
208 let is_volume_rider = ecs.read_storage::<Is<VolumeRider>>();
209
210 let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
211 (e.floor() as i32).div_euclid(sz as i32)
212 });
213 let player_body = bodies.get(player_entity);
214 let player_mass = masses.get(player_entity);
215 let Some(player_uid) = uids.get(player_entity).copied() else {
216 tracing::error!("Client has no Uid component! Not scanning for any interactables.");
217 return Err(GetInteractablesError::ClientMissingUid);
218 };
219
220 let spacial_grid = ecs.read_resource::<CachedSpatialGrid>();
221
222 let entities = ecs.entities();
223 let mut entity_data = (
224 &entities,
225 !&is_mounts,
226 &uids,
227 &positions,
228 &bodies,
229 &masses,
230 char_states.maybe(),
231 healths.maybe(),
232 alignments.maybe(),
233 items.mask().maybe(),
234 )
235 .lend_join();
236
237 let interactable_entities = spacial_grid
238 .0
239 .in_circle_aabr(player_pos.xy(), MAX_PICKUP_RANGE)
240 .chain(entity_target.map(|t| t.kind.0))
241 .filter(|&entity| entity != player_entity)
242 .filter_map(|entity| entity_data.get(entity, &entities))
243 .flat_map(
244 |(entity, _, uid, pos, body, mass, char_state, health, alignment, has_item)| {
245 let is_downed = comp::is_downed(health, char_state);
247
248 let interaction = if is_downed {
250 Some(EntityInteraction::HelpDowned)
251 } else if has_item.is_some() {
252 Some(EntityInteraction::PickupItem)
253 } else if body.is_portal()
254 && pos.0.distance_squared(player_pos) <= TELEPORTER_RADIUS.powi(2)
255 {
256 Some(EntityInteraction::ActivatePortal)
257 } else if alignment.is_some_and(|alignment| {
258 can_perform_pet(comp::Pos(player_pos), *pos, *alignment)
259 }) {
260 Some(EntityInteraction::Pet)
261 } else if alignment.is_some_and(|alignment| matches!(alignment, Alignment::Npc)) {
262 Some(EntityInteraction::Talk)
263 } else {
264 None
265 };
266
267 let sit =
269 (body.is_campfire() && !is_downed).then_some(EntityInteraction::CampfireSit);
270
271 let trade = (!is_downed
275 && alignment.is_some_and(|alignment| match alignment {
276 Alignment::Npc => true,
277 Alignment::Owned(other_uid) => other_uid == uid || player_uid == *other_uid,
278 _ => false,
279 }))
280 .then_some(EntityInteraction::Trade);
281
282 let mount = (matches!(alignment, Some(Alignment::Owned(other_uid)) if player_uid == *other_uid)
284 && pet::is_mountable(body, mass, player_body, player_mass)
285 && !is_downed
286 && !client.is_riding())
287 .then_some(EntityInteraction::Mount);
288
289 let stayfollow = alignment
291 .filter(|alignment| {
292 matches!(alignment,
293 Alignment::Owned(other_uid) if player_uid == *other_uid)
294 && !is_downed
295 })
296 .map(|_| EntityInteraction::StayFollow);
297
298 let distance_squared = player_pos.distance_squared(pos.0);
300
301 Some(
302 interaction
303 .into_iter()
304 .chain(sit)
305 .chain(trade)
306 .chain(mount)
307 .chain(stayfollow)
308 .filter_map(move |interaction| {
309 (distance_squared <= interaction.range().powi(2)).then_some((
310 interaction,
311 entity,
312 distance_squared,
313 ))
314 }),
315 )
316 },
317 )
318 .flatten();
319
320 let volumes_data = (
321 &entities,
322 &ecs.read_storage::<Uid>(),
323 &ecs.read_storage::<comp::Body>(),
324 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
325 &ecs.read_storage::<comp::Collider>(),
326 );
327
328 let mut volumes_data = volumes_data.lend_join();
329
330 let volume_interactables = spacial_grid
331 .0
332 .in_circle_aabr(player_pos.xy(), MAX_PICKUP_RANGE)
333 .filter(|&e| e != player_entity)
334 .filter_map(|e| volumes_data.get(e, &entities))
335 .filter_map(|(entity, uid, body, interpolated, collider)| {
336 let vol = collider.get_vol(&voxel_colliders_manifest)?;
337 let (blocks_of_interest, offset) =
338 scene
339 .figure_mgr()
340 .get_blocks_of_interest(entity, body, Some(collider))?;
341
342 let mat = Mat4::from(interpolated.ori.to_quat()).translated_3d(interpolated.pos)
343 * Mat4::translation_3d(offset);
344
345 let p = mat.inverted().mul_point(player_pos);
346 let aabb = Aabb {
347 min: Vec3::zero(),
348 max: vol.volume().sz.as_(),
349 };
350 if aabb.contains_point(p) || aabb.distance_to_point(p) < MAX_PICKUP_RANGE {
351 Some(blocks_of_interest.interactables.iter().map(
352 move |(block_offset, interaction)| {
353 let wpos = mat.mul_point(block_offset.as_() + 0.5);
354 (wpos, VolumePos::entity(*block_offset, *uid), *interaction)
355 },
356 ))
357 } else {
358 None
359 }
360 })
361 .flatten();
362
363 let interactable_blocks = Spiral2d::new()
366 .take(
367 ((MAX_PICKUP_RANGE / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2),
368 )
369 .flat_map(|offset| {
370 let chunk_pos = player_chunk + offset;
371 let chunk_voxel_pos =
372 Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
373 scene_terrain
374 .get(chunk_pos)
375 .map(|data| (data, chunk_voxel_pos))
376 })
377 .flat_map(|(chunk_data, chunk_pos)| {
378 chunk_data
381 .blocks_of_interest
382 .interactables
383 .iter()
384 .map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
385 .map(|(pos, interaction)| {
386 (
387 pos.as_::<f32>() + 0.5,
388 VolumePos::terrain(pos),
389 *interaction,
390 )
391 })
392 })
393 .chain(volume_interactables)
394 .filter(|(wpos, volume_pos, interaction)| match interaction {
395 Interaction::Mount => {
396 !is_volume_rider.contains(player_entity)
397 && wpos.distance_squared(player_pos) < MAX_SPRITE_MOUNT_RANGE.powi(2)
398 && (volume_pos.is_entity()
400 || !is_volume_rider
401 .join()
402 .any(|is_volume_rider| is_volume_rider.pos == *volume_pos))
403 },
404 Interaction::LightToggle(_) => {
405 wpos.distance_squared(player_pos) < MAX_INTERACT_RANGE.powi(2)
406 },
407 _ => true,
408 })
409 .chain(
410 mine_target
411 .map(|t| t.position_int())
412 .into_iter()
413 .chain(collect_target.map(|t| t.position_int()))
414 .map(|pos| (pos.as_(), VolumePos::terrain(pos), Interaction::Collect)),
415 )
416 .filter_map(|(wpos, volume_pos, interaction)| {
417 let (block, interaction) = BlockInteraction::from_block_pos(
418 &terrain,
419 &id_maps,
420 &colliders,
421 volume_pos,
422 interaction,
423 )?;
424
425 let distance = wpos.distance_squared(player_pos);
426 (distance <= interaction.range().powi(2)).then_some((
427 block,
428 volume_pos,
429 interaction,
430 distance,
431 ))
432 });
433
434 let is_direct_target = |interactable: &Interactable| match interactable {
437 Interactable::Block {
438 volume_pos,
439 interaction,
440 ..
441 } => {
442 matches!(
443 (mine_target, volume_pos, interaction),
444 (Some(target), VolumePos { kind: Volume::Terrain, pos }, BlockInteraction::Mine(_))
445 if target.position_int() == *pos)
446 || matches!(
447 (collect_target, volume_pos, interaction),
448 (Some(target), VolumePos { kind: Volume::Terrain, pos }, BlockInteraction::Collect { .. } | BlockInteraction::Unlock { .. })
449 if target.position_int() == *pos)
450 },
451 Interactable::Entity { entity, .. } => {
452 entity_target.is_some_and(|target| target.kind.0 == *entity)
453 },
454 };
455
456 Ok(interactable_entities
457 .map(|(interaction, entity, distance)| {
458 (distance.powi(2), Interactable::Entity {
459 entity,
460 interaction,
461 })
462 })
463 .chain(
464 interactable_blocks.map(|(block, volume_pos, interaction, distance_squared)| {
465 (distance_squared, Interactable::Block {
466 block,
467 volume_pos,
468 interaction,
469 })
470 }),
471 )
472 .fold(HashMap::new(), |mut map, (distance, interaction)| {
473 let input = interaction.game_input();
474
475 if map
476 .get(&input)
477 .is_none_or(|(other_distance, other_interaction)| {
478 (
479 is_direct_target(other_interaction),
481 other_interaction.priority(),
482 Reverse(*other_distance),
483 ) < (
484 is_direct_target(&interaction),
485 interaction.priority(),
486 Reverse(distance),
487 )
488 })
489 {
490 map.insert(input, (distance, interaction));
491 }
492
493 map
494 }))
495}
496
497impl Interactable {
498 pub fn game_input(&self) -> GameInput {
499 match self {
500 Interactable::Block { interaction, .. } => interaction.game_input(),
501 Interactable::Entity { interaction, .. } => interaction.game_input(),
502 }
503 }
504
505 #[rustfmt::skip]
508 pub fn priority(&self) -> usize {
509 match self {
510 Self::Entity { interaction: EntityInteraction::ActivatePortal, .. } => 4,
512 Self::Entity { interaction: EntityInteraction::PickupItem, .. } => 3,
513 Self::Block { interaction: BlockInteraction::Craft(_), .. } => 3,
514 Self::Block { interaction: BlockInteraction::Collect { .. }, .. } => 3,
515 Self::Entity { interaction: EntityInteraction::HelpDowned, .. } => 2,
516 Self::Block { interaction: BlockInteraction::Unlock(_), .. } => 1,
517 Self::Block { interaction: BlockInteraction::Read(_), .. } => 1,
518 Self::Block { interaction: BlockInteraction::LightToggle(_), .. } => 1,
519 Self::Entity { interaction: EntityInteraction::Pet, .. } => 0,
520 Self::Entity { interaction: EntityInteraction::Talk , .. } => 0,
521
522 Self::Entity { interaction: EntityInteraction::Mount, .. } => 1,
524 Self::Block { interaction: BlockInteraction::Mount, .. } => 0,
525
526 Self::Block { interaction: BlockInteraction::Mine(_), .. } => 0,
528 Self::Entity { interaction: EntityInteraction::StayFollow, .. } => 0,
529 Self::Entity { interaction: EntityInteraction::Trade, .. } => 0,
530 Self::Entity { interaction: EntityInteraction::CampfireSit, .. } => 0,
531 }
532 }
533}
534
535impl EntityInteraction {
536 pub fn game_input(&self) -> GameInput {
537 match self {
538 EntityInteraction::HelpDowned
539 | EntityInteraction::PickupItem
540 | EntityInteraction::ActivatePortal
541 | EntityInteraction::Pet
542 | EntityInteraction::Talk => GameInput::Interact,
543 EntityInteraction::StayFollow => GameInput::StayFollow,
544 EntityInteraction::Trade => GameInput::Trade,
545 EntityInteraction::Mount => GameInput::Mount,
546 EntityInteraction::CampfireSit => GameInput::Sit,
547 }
548 }
549
550 pub fn range(&self) -> f32 {
551 match self {
552 Self::Trade => consts::MAX_TRADE_RANGE,
553 Self::Mount | Self::Pet => consts::MAX_MOUNT_RANGE,
554 Self::PickupItem => consts::MAX_PICKUP_RANGE,
555 Self::Talk => consts::MAX_NPCINTERACT_RANGE,
556 _ => consts::MAX_INTERACT_RANGE,
557 }
558 }
559}
560
561impl Interactables {
562 pub fn inverted_map(
564 &self,
565 ) -> (
566 HashMap<specs::Entity, Vec<EntityInteraction>>,
567 HashMap<VolumePos, (Block, Vec<&BlockInteraction>)>,
568 ) {
569 let (mut entity_map, block_map) = self.input_map.iter().fold(
570 (HashMap::new(), HashMap::new()),
571 |(mut entity_map, mut block_map), (_input, (_, interactable))| {
572 match interactable {
573 Interactable::Entity {
574 entity,
575 interaction,
576 } => {
577 entity_map
578 .entry(*entity)
579 .and_modify(|i: &mut Vec<_>| i.push(*interaction))
580 .or_insert_with(|| vec![*interaction]);
581 },
582 Interactable::Block {
583 block,
584 volume_pos,
585 interaction,
586 } => {
587 block_map
588 .entry(*volume_pos)
589 .and_modify(|(_, i): &mut (_, Vec<_>)| i.push(interaction))
590 .or_insert_with(|| (*block, vec![interaction]));
591 },
592 }
593
594 (entity_map, block_map)
595 },
596 );
597
598 for v in entity_map.values_mut() {
602 v.sort_unstable_by_key(|i| i.game_input())
603 }
604
605 (entity_map, block_map)
606 }
607}