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