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 scene: &Scene,
181) -> Result<HashMap<GameInput, (f32, Interactable)>, GetInteractablesError> {
182 span!(_guard, "select_interactable");
183 use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
184
185 let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
186
187 let ecs = client.state().ecs();
188 let id_maps = ecs.read_resource::<IdMaps>();
189 let scene_terrain = scene.terrain();
190 let terrain = client.state().terrain();
191 let player_entity = client.entity();
192 let interpolated = ecs.read_storage::<crate::ecs::comp::Interpolated>();
193 let player_pos = interpolated
194 .get(player_entity)
195 .ok_or(GetInteractablesError::ClientMissingPosition)?
196 .pos;
197
198 let uids = ecs.read_storage::<Uid>();
199 let healths = ecs.read_storage::<comp::Health>();
200 let colliders = ecs.read_storage::<comp::Collider>();
201 let char_states = ecs.read_storage::<comp::CharacterState>();
202 let is_mounts = ecs.read_storage::<Is<Mount>>();
203 let bodies = ecs.read_storage::<comp::Body>();
204 let masses = ecs.read_storage::<comp::Mass>();
205 let items = ecs.read_storage::<comp::PickupItem>();
206 let alignments = ecs.read_storage::<comp::Alignment>();
207 let is_volume_rider = ecs.read_storage::<Is<VolumeRider>>();
208 let volume_riders = ecs.read_storage::<common::mounting::VolumeRiders>();
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 &interpolated,
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(), EntityInteraction::max_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, interpolated, 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 Some(EntityInteraction::ActivatePortal)
255 } else if alignment.is_some_and(|alignment| {
256 can_perform_pet(comp::Pos(player_pos), comp::Pos(interpolated.pos), *alignment)
257 }) {
258 Some(EntityInteraction::Pet)
259 } else if alignment.is_some_and(|alignment| matches!(alignment, Alignment::Npc)) {
260 Some(EntityInteraction::Talk)
261 } else {
262 None
263 };
264
265 let sit =
267 (body.is_campfire() && !is_downed).then_some(EntityInteraction::CampfireSit);
268
269 let trade = (!is_downed
273 && alignment.is_some_and(|alignment| match alignment {
274 Alignment::Npc => true,
275 Alignment::Owned(other_uid) => other_uid == uid || player_uid == *other_uid,
276 _ => false,
277 }))
278 .then_some(EntityInteraction::Trade);
279
280 let mount = (matches!(alignment, Some(Alignment::Owned(other_uid)) if player_uid == *other_uid)
282 && pet::is_mountable(body, mass, player_body, player_mass)
283 && !is_downed
284 && !client.is_riding())
285 .then_some(EntityInteraction::Mount);
286
287 let stayfollow = alignment
289 .filter(|alignment| {
290 matches!(alignment,
291 Alignment::Owned(other_uid) if player_uid == *other_uid)
292 && !is_downed
293 })
294 .map(|_| EntityInteraction::StayFollow);
295
296 let distance_squared = player_pos.distance_squared(interpolated.pos);
298
299 [interaction, sit, trade, mount, stayfollow].into_iter().flatten()
300 .filter_map(move |interaction| {
301 (distance_squared <= interaction.range().powi(2)).then_some((
302 interaction,
303 entity,
304 distance_squared,
305 ))
306 })
307 },
308 );
309
310 let volumes_data = (
311 &entities,
312 &ecs.read_storage::<Uid>(),
313 &ecs.read_storage::<comp::Body>(),
314 &ecs.read_storage::<crate::ecs::comp::Interpolated>(),
315 &ecs.read_storage::<comp::Collider>(),
316 );
317
318 let mut volumes_data = volumes_data.lend_join();
319
320 let volume_interactables = spacial_grid
321 .0
322 .in_circle_aabr(player_pos.xy(), BlockInteraction::max_range())
323 .filter(|&e| e != player_entity)
324 .filter_map(|e| volumes_data.get(e, &entities))
325 .filter_map(|(entity, uid, body, interpolated, collider)| {
326 let vol = collider.get_vol(&voxel_colliders_manifest)?;
327 let (blocks_of_interest, offset) =
328 scene
329 .figure_mgr()
330 .get_blocks_of_interest(entity, body, Some(collider))?;
331
332 let mat = Mat4::from(interpolated.ori.to_quat()).translated_3d(interpolated.pos)
333 * Mat4::translation_3d(offset);
334
335 let p = mat.inverted().mul_point(player_pos);
336 let aabb = Aabb {
337 min: Vec3::zero(),
338 max: vol.volume().sz.as_(),
339 };
340
341 if aabb.contains_point(p) || aabb.distance_to_point(p) < BlockInteraction::max_range() {
342 Some(blocks_of_interest.interactables.iter().map(
343 move |(block_offset, interaction)| {
344 let wpos = mat.mul_point(block_offset.as_() + 0.5);
345 (wpos, VolumePos::entity(*block_offset, *uid), *interaction)
346 },
347 ))
348 } else {
349 None
350 }
351 })
352 .flatten();
353
354 let interactable_blocks = Spiral2d::new()
357 .take(
358 ((BlockInteraction::max_range() / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize
359 * 2
360 + 1)
361 .pow(2),
362 )
363 .flat_map(|offset| {
364 let chunk_pos = player_chunk + offset;
365 let chunk_voxel_pos =
366 Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
367 scene_terrain
368 .get(chunk_pos)
369 .map(|data| (data, chunk_voxel_pos))
370 })
371 .flat_map(|(chunk_data, chunk_pos)| {
372 chunk_data
375 .blocks_of_interest
376 .interactables
377 .iter()
378 .map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
379 .map(|(pos, interaction)| {
380 (
381 pos.as_::<f32>() + 0.5,
382 VolumePos::terrain(pos),
383 *interaction,
384 )
385 })
386 })
387 .chain(volume_interactables)
388 .chain(
389 mine_target
390 .map(|t| t.position_int())
391 .into_iter()
392 .chain(collect_target.map(|t| t.position_int()))
393 .map(|pos| (pos.as_(), VolumePos::terrain(pos), Interaction::Collect)),
394 )
395 .filter_map(|(wpos, volume_pos, interaction)| {
396 let distance_sq = wpos.distance_squared(player_pos);
397 if distance_sq > BlockInteraction::max_range().powi(2) {
398 return None;
399 }
400
401 let (block, interaction) = BlockInteraction::from_block_pos(
402 &terrain,
403 &id_maps,
404 &colliders,
405 volume_pos,
406 interaction,
407 )?;
408
409 (distance_sq <= interaction.range().powi(2)).then_some((
410 block,
411 volume_pos,
412 interaction,
413 distance_sq,
414 ))
415 })
416 .filter(|(_, volume_pos, interaction, _)| {
417 if let BlockInteraction::Mount = interaction {
419 !is_volume_rider.contains(player_entity)
420 && match volume_pos.kind {
423 Volume::Terrain => !is_volume_rider
424 .join()
425 .any(|is_volume_rider| is_volume_rider.pos == *volume_pos),
426 Volume::Entity(volume_uid) => {
427 id_maps.uid_entity(volume_uid).is_some_and(|volume_entity| {
428 volume_riders
430 .get(volume_entity)
431 .is_none_or(|riders| !riders.spot_taken(volume_pos.pos))
432 })
433 },
434 }
435 } else {
436 true
437 }
438 });
439
440 let is_direct_target = |interactable: &Interactable| match interactable {
443 Interactable::Block {
444 volume_pos,
445 interaction,
446 ..
447 } => {
448 matches!(
449 (mine_target, volume_pos, interaction),
450 (Some(target), VolumePos { kind: Volume::Terrain, pos }, BlockInteraction::Mine(_))
451 if target.position_int() == *pos)
452 || matches!(
453 (collect_target, volume_pos, interaction),
454 (Some(target), VolumePos { kind: Volume::Terrain, pos }, BlockInteraction::Collect { .. } | BlockInteraction::Unlock { .. })
455 if target.position_int() == *pos)
456 },
457 Interactable::Entity { entity, .. } => {
458 entity_target.is_some_and(|target| target.kind.0 == *entity)
459 },
460 };
461
462 Ok(interactable_entities
463 .map(|(interaction, entity, distance)| {
464 (distance.powi(2), Interactable::Entity {
465 entity,
466 interaction,
467 })
468 })
469 .chain(
470 interactable_blocks.map(|(block, volume_pos, interaction, distance_squared)| {
471 (distance_squared, Interactable::Block {
472 block,
473 volume_pos,
474 interaction,
475 })
476 }),
477 )
478 .fold(HashMap::new(), |mut map, (distance_sq, interaction)| {
479 let input = interaction.game_input();
480
481 if map
482 .get(&input)
483 .is_none_or(|(other_distance_sq, other_interaction)| {
484 (
485 is_direct_target(other_interaction),
487 other_interaction.priority(),
488 Reverse(*other_distance_sq),
489 ) < (
490 is_direct_target(&interaction),
491 interaction.priority(),
492 Reverse(distance_sq),
493 )
494 })
495 {
496 map.insert(input, (distance_sq, interaction));
497 }
498
499 map
500 }))
501}
502
503impl Interactable {
504 fn game_input(&self) -> GameInput {
505 match self {
506 Interactable::Block { interaction, .. } => interaction.game_input(),
507 Interactable::Entity { interaction, .. } => interaction.game_input(),
508 }
509 }
510
511 #[rustfmt::skip]
514 fn priority(&self) -> usize {
515 match self {
516 Self::Entity { interaction: EntityInteraction::ActivatePortal, .. } => 4,
518 Self::Entity { interaction: EntityInteraction::PickupItem, .. } => 3,
519 Self::Block { interaction: BlockInteraction::Craft(_), .. } => 3,
520 Self::Block { interaction: BlockInteraction::Collect { .. }, .. } => 3,
521 Self::Entity { interaction: EntityInteraction::HelpDowned, .. } => 2,
522 Self::Block { interaction: BlockInteraction::Unlock { .. }, .. } => 1,
523 Self::Block { interaction: BlockInteraction::Read(_), .. } => 1,
524 Self::Block { interaction: BlockInteraction::LightToggle(_), .. } => 1,
525 Self::Entity { interaction: EntityInteraction::Pet, .. } => 0,
526 Self::Entity { interaction: EntityInteraction::Talk , .. } => 0,
527
528 Self::Entity { interaction: EntityInteraction::Mount, .. } => 1,
530 Self::Block { interaction: BlockInteraction::Mount, .. } => 0,
531
532 Self::Block { interaction: BlockInteraction::Mine(_), .. } => 0,
534 Self::Entity { interaction: EntityInteraction::StayFollow, .. } => 0,
535 Self::Entity { interaction: EntityInteraction::Trade, .. } => 0,
536 Self::Entity { interaction: EntityInteraction::CampfireSit, .. } => 0,
537 }
538 }
539}
540
541impl EntityInteraction {
542 pub(crate) fn game_input(&self) -> GameInput {
543 match self {
544 EntityInteraction::HelpDowned
545 | EntityInteraction::PickupItem
546 | EntityInteraction::ActivatePortal
547 | EntityInteraction::Pet
548 | EntityInteraction::Talk => GameInput::Interact,
549 EntityInteraction::StayFollow => GameInput::StayFollow,
550 EntityInteraction::Trade => GameInput::Trade,
551 EntityInteraction::Mount => GameInput::Mount,
552 EntityInteraction::CampfireSit => GameInput::Sit,
553 }
554 }
555
556 fn range(&self) -> f32 {
557 match self {
559 Self::Trade => consts::MAX_TRADE_RANGE,
560 Self::Mount | Self::Pet => consts::MAX_MOUNT_RANGE,
561 Self::PickupItem => consts::MAX_PICKUP_RANGE,
562 Self::Talk => consts::MAX_NPCINTERACT_RANGE,
563 Self::CampfireSit => consts::MAX_CAMPFIRE_RANGE,
564 Self::ActivatePortal => consts::TELEPORTER_RADIUS,
565 Self::HelpDowned | Self::StayFollow => consts::MAX_INTERACT_RANGE,
566 }
567 }
568
569 fn max_range() -> f32 {
570 consts::MAX_TRADE_RANGE
571 .max(consts::MAX_MOUNT_RANGE)
572 .max(consts::MAX_PICKUP_RANGE)
573 .max(consts::MAX_NPCINTERACT_RANGE)
574 .max(consts::MAX_CAMPFIRE_RANGE)
575 .max(consts::MAX_INTERACT_RANGE)
576 }
577}
578
579impl Interactables {
580 pub fn inverted_map(
582 &self,
583 ) -> (
584 HashMap<specs::Entity, Vec<EntityInteraction>>,
585 HashMap<VolumePos, (Block, Vec<&BlockInteraction>)>,
586 ) {
587 let (mut entity_map, block_map) = self.input_map.iter().fold(
588 (HashMap::new(), HashMap::new()),
589 |(mut entity_map, mut block_map), (_input, (_, interactable))| {
590 match interactable {
591 Interactable::Entity {
592 entity,
593 interaction,
594 } => {
595 entity_map
596 .entry(*entity)
597 .and_modify(|i: &mut Vec<_>| i.push(*interaction))
598 .or_insert_with(|| vec![*interaction]);
599 },
600 Interactable::Block {
601 block,
602 volume_pos,
603 interaction,
604 } => {
605 block_map
606 .entry(*volume_pos)
607 .and_modify(|(_, i): &mut (_, Vec<_>)| i.push(interaction))
608 .or_insert_with(|| (*block, vec![interaction]));
609 },
610 }
611
612 (entity_map, block_map)
613 },
614 );
615
616 for v in entity_map.values_mut() {
620 v.sort_unstable_by_key(|i| i.game_input())
621 }
622
623 (entity_map, block_map)
624 }
625}