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