veloren_server/events/
interaction.rs

1use std::{f32::consts::PI, ops::Mul};
2
3use common::rtsim::DialogueKind;
4use common_state::{BlockChange, ScheduledBlockChange};
5use specs::{DispatcherBuilder, Join, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
6use tracing::error;
7use vek::*;
8
9use common::{
10    assets::{AssetCombined, AssetHandle, Ron},
11    comp::{
12        self, InventoryUpdateEvent,
13        agent::{AgentEvent, Sound, SoundKind},
14        inventory::slot::EquipSlot,
15        item::{MaterialStatManifest, flatten_counted_items},
16        loot_owner::LootOwnerKind,
17        tool::AbilityMap,
18    },
19    consts::{MAX_INTERACT_RANGE, MAX_NPCINTERACT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
20    event::{
21        CreateItemDropEvent, CreateSpriteEvent, DialogueEvent, EventBus, MineBlockEvent,
22        NpcInteractEvent, SetLanternEvent, SetPetStayEvent, SoundEvent, TamePetEvent,
23        ToggleSpriteLightEvent,
24    },
25    link::Is,
26    mounting::Mount,
27    outcome::Outcome,
28    resources::ProgramTime,
29    terrain::{self, Block, SpriteKind, TerrainGrid},
30    uid::Uid,
31    util::Dir,
32    vol::ReadVol,
33};
34
35use crate::{Server, ServerGeneral, Time, client::Client};
36
37use crate::pet::tame_pet;
38use hashbrown::{HashMap, HashSet};
39use lazy_static::lazy_static;
40
41use super::{ServerEvent, event_dispatch, mounting::within_mounting_range};
42
43pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
44    event_dispatch::<SetLanternEvent>(builder, &[]);
45    event_dispatch::<NpcInteractEvent>(builder, &[]);
46    event_dispatch::<DialogueEvent>(builder, &[]);
47    event_dispatch::<SetPetStayEvent>(builder, &[]);
48    event_dispatch::<MineBlockEvent>(builder, &[]);
49    event_dispatch::<SoundEvent>(builder, &[]);
50    event_dispatch::<CreateSpriteEvent>(builder, &[]);
51    event_dispatch::<ToggleSpriteLightEvent>(builder, &[]);
52}
53
54impl ServerEvent for SetLanternEvent {
55    type SystemData<'a> = (
56        WriteStorage<'a, comp::LightEmitter>,
57        ReadStorage<'a, comp::Inventory>,
58        ReadStorage<'a, comp::Health>,
59    );
60
61    fn handle(
62        events: impl ExactSizeIterator<Item = Self>,
63        (mut light_emitters, inventories, healths): Self::SystemData<'_>,
64    ) {
65        for SetLanternEvent(entity, enable) in events {
66            let lantern_exists = light_emitters
67                .get(entity)
68                .is_some_and(|light| light.strength > 0.0);
69
70            if lantern_exists != enable {
71                if !enable {
72                    light_emitters.remove(entity);
73                }
74                // Only enable lantern if entity is alive
75                else if healths.get(entity).is_none_or(|h| !h.is_dead) {
76                    let lantern_info = inventories
77                        .get(entity)
78                        .and_then(|inventory| inventory.equipped(EquipSlot::Lantern))
79                        .and_then(|item| {
80                            if let comp::item::ItemKind::Lantern(l) = &*item.kind() {
81                                Some((l.color(), l.strength(), l.flicker()))
82                            } else {
83                                None
84                            }
85                        });
86                    if let Some((col, strength, flicker)) = lantern_info {
87                        let _ = light_emitters.insert(entity, comp::LightEmitter {
88                            col,
89                            strength,
90                            flicker,
91                            animated: true,
92                        });
93                    }
94                }
95            }
96        }
97    }
98}
99
100impl ServerEvent for NpcInteractEvent {
101    type SystemData<'a> = (
102        WriteStorage<'a, comp::Agent>,
103        ReadStorage<'a, comp::Pos>,
104        ReadStorage<'a, Uid>,
105    );
106
107    fn handle(
108        events: impl ExactSizeIterator<Item = Self>,
109        (mut agents, positions, uids): Self::SystemData<'_>,
110    ) {
111        for NpcInteractEvent(interactor, npc_entity) in events {
112            let within_range = {
113                positions
114                    .get(interactor)
115                    .zip(positions.get(npc_entity))
116                    .is_some_and(|(interactor_pos, npc_pos)| {
117                        interactor_pos.0.distance_squared(npc_pos.0)
118                            <= MAX_NPCINTERACT_RANGE.powi(2)
119                    })
120            };
121
122            if within_range
123                && let Some(agent) = agents.get_mut(npc_entity)
124                && agent.target.is_none()
125                && let Some(interactor_uid) = uids.get(interactor)
126            {
127                agent.inbox.push_back(AgentEvent::Talk(*interactor_uid));
128            }
129        }
130    }
131}
132
133impl ServerEvent for DialogueEvent {
134    type SystemData<'a> = (
135        ReadStorage<'a, Uid>,
136        ReadStorage<'a, comp::Pos>,
137        ReadStorage<'a, Client>,
138        WriteStorage<'a, comp::Agent>,
139        WriteStorage<'a, comp::Inventory>,
140        ReadExpect<'a, AbilityMap>,
141        ReadExpect<'a, MaterialStatManifest>,
142        WriteStorage<'a, comp::InventoryUpdate>,
143    );
144
145    fn handle(
146        events: impl ExactSizeIterator<Item = Self>,
147        (
148            uids,
149            positions,
150            clients,
151            mut agents,
152            mut inventories,
153            ability_map,
154            msm,
155            mut inventory_updates,
156        ): Self::SystemData<'_>,
157    ) {
158        for DialogueEvent(sender, target, dialogue) in events {
159            let within_range = positions
160                .get(sender)
161                .zip(positions.get(target))
162                .is_some_and(|(sender_pos, target_pos)| {
163                    sender_pos.0.distance_squared(target_pos.0) <= MAX_NPCINTERACT_RANGE.powi(2)
164                });
165
166            if within_range && let Some(sender_uid) = uids.get(sender) {
167                // Perform item transfer, if required
168                let given_item = match &dialogue.kind {
169                    DialogueKind::Start
170                    | DialogueKind::End
171                    | DialogueKind::Question { .. }
172                    | DialogueKind::Marker { .. }
173                    | DialogueKind::Ack { .. } => None,
174                    DialogueKind::Statement { given_item, .. } => given_item.as_ref(),
175                    DialogueKind::Response { response, .. } => response.given_item.as_ref(),
176                };
177                // If the response requires an item to be given, perform exchange (or exit)
178                if let Some((item_def, amount)) = given_item {
179                    // Check that the target's inventory has enough space for the item
180                    if let Some(target_inv) = inventories.get(target)
181                        && target_inv.has_space_for(item_def, *amount)
182                        // Check that the sender has enough of the item
183                        && let Some(mut sender_inv) = inventories.get_mut(sender)
184                        && sender_inv.item_count(item_def) >= *amount as u64
185                        // First, remove the item from the sender's inventory
186                        && let Some(items) = sender_inv.remove_item_amount(item_def, *amount, &ability_map, &msm)
187                        && let Some(mut target_inv) = inventories.get_mut(target)
188                    {
189                        for item in items {
190                            let item_event = InventoryUpdateEvent::Collected(
191                                item.frontend_item(&ability_map, &msm),
192                            );
193                            // Push the items to the target's inventory
194                            if target_inv.push(item).is_err() {
195                                error!(
196                                    "Failed to insert dialogue given item despite target \
197                                     inventory claiming to have space, dropping remaining items..."
198                                );
199                                break;
200                            } else {
201                                inventory_updates
202                                    .insert(target, comp::InventoryUpdate::new(item_event))
203                                    .expect("The entity must exist because we have its inventory");
204                            }
205                        }
206                    } else {
207                        // TODO: Respond with error message on failure?
208                        continue;
209                    }
210                }
211
212                let dialogue = dialogue.into_validated_unchecked();
213
214                if let Some(agent) = agents.get_mut(target) {
215                    agent
216                        .inbox
217                        .push_back(AgentEvent::Dialogue(*sender_uid, dialogue.clone()));
218                }
219
220                if let Some(client) = clients.get(target) {
221                    client.send_fallible(ServerGeneral::Dialogue(*sender_uid, dialogue));
222                }
223            }
224        }
225    }
226}
227
228impl ServerEvent for SetPetStayEvent {
229    type SystemData<'a> = (
230        WriteStorage<'a, comp::Agent>,
231        WriteStorage<'a, comp::CharacterActivity>,
232        ReadStorage<'a, comp::Pos>,
233        ReadStorage<'a, comp::Alignment>,
234        ReadStorage<'a, Is<Mount>>,
235        ReadStorage<'a, Uid>,
236    );
237
238    fn handle(
239        events: impl ExactSizeIterator<Item = Self>,
240        (mut agents, mut character_activities, positions, alignments, is_mounts, uids): Self::SystemData<'_>,
241    ) {
242        for SetPetStayEvent(command_giver, pet, stay) in events {
243            let is_owner = uids.get(command_giver).is_some_and(|owner_uid| {
244                matches!(
245                    alignments.get(pet),
246                    Some(comp::Alignment::Owned(pet_owner)) if *pet_owner == *owner_uid,
247                )
248            });
249
250            let current_pet_position = positions.get(pet).copied();
251            let stay = stay && current_pet_position.is_some();
252            if is_owner
253                && within_mounting_range(positions.get(command_giver), positions.get(pet))
254                && is_mounts.get(pet).is_none()
255            {
256                character_activities
257                    .get_mut(pet)
258                    .map(|mut activity| activity.is_pet_staying = stay);
259                agents
260                    .get_mut(pet)
261                    .map(|s| s.stay_pos = current_pet_position.filter(|_| stay));
262            }
263        }
264    }
265}
266
267lazy_static! {
268    static ref RESOURCE_EXPERIENCE_MANIFEST: AssetHandle<Ron<HashMap<String, u32>>> =
269        Ron::load_expect_combined_static("server.manifests.resource_experience_manifest");
270}
271
272impl ServerEvent for MineBlockEvent {
273    type SystemData<'a> = (
274        WriteExpect<'a, BlockChange>,
275        ReadExpect<'a, TerrainGrid>,
276        ReadExpect<'a, MaterialStatManifest>,
277        ReadExpect<'a, AbilityMap>,
278        ReadExpect<'a, EventBus<CreateItemDropEvent>>,
279        ReadExpect<'a, EventBus<SoundEvent>>,
280        ReadExpect<'a, EventBus<Outcome>>,
281        ReadExpect<'a, ProgramTime>,
282        ReadExpect<'a, Time>,
283        WriteStorage<'a, comp::SkillSet>,
284        ReadStorage<'a, Uid>,
285    );
286
287    fn handle(
288        events: impl ExactSizeIterator<Item = Self>,
289        (
290            mut block_change,
291            terrain,
292            msm,
293            ability_map,
294            create_item_drop_events,
295            sound_events,
296            outcomes,
297            program_time,
298            time,
299            mut skill_sets,
300            uids,
301        ): Self::SystemData<'_>,
302    ) {
303        use rand::Rng;
304        let mut rng = rand::rng();
305        let mut create_item_drop_emitter = create_item_drop_events.emitter();
306        let mut sound_event_emitter = sound_events.emitter();
307        let mut outcome_emitter = outcomes.emitter();
308        for ev in events {
309            if block_change.can_set_block(ev.pos) {
310                let block = terrain.get(ev.pos).ok().copied();
311                if let Some(mut block) =
312                    block.filter(|b| b.mine_tool().is_some_and(|t| Some(t) == ev.tool))
313                {
314                    // Attempt to increase the resource's damage
315                    let damage = if let Ok(damage) = block.get_attr::<terrain::sprite::Damage>() {
316                        let updated_damage = damage.0.saturating_add(1);
317                        block
318                            .set_attr(terrain::sprite::Damage(updated_damage))
319                            .expect(
320                                "We just read the Damage attribute from the block, writing should \
321                                 be possible too",
322                            );
323
324                        Some(updated_damage)
325                    } else {
326                        None
327                    };
328
329                    let sprite = block.get_sprite();
330
331                    // Maximum damage has reached, destroy the block
332                    let is_broken = damage
333                        .and_then(|damage| Some((sprite?.required_mine_damage(), damage)))
334                        .is_some_and(|(required_damage, damage)| {
335                            required_damage.is_none_or(|required| damage >= required)
336                        });
337
338                    // Stage changes happen in damage interval of `mine_drop_intevral`
339                    let stage_changed = damage
340                        .and_then(|damage| Some((sprite?.mine_drop_interval(), damage)))
341                        .is_some_and(|(interval, damage)| damage % interval == 0);
342
343                    let sprite_cfg = terrain.sprite_cfg_at(ev.pos);
344                    if (stage_changed || is_broken)
345                        && let Some(items) = comp::Item::try_reclaim_from_block(block, sprite_cfg)
346                    {
347                        let mut items: Vec<_> =
348                            flatten_counted_items(&items, &ability_map, &msm).collect();
349                        let maybe_uid = uids.get(ev.entity).copied();
350
351                        if let Some(mut skillset) = skill_sets.get_mut(ev.entity) {
352                            use common::comp::skills::{MiningSkill, SKILL_MODIFIERS, Skill};
353
354                            if is_broken
355                                && let (Some(tool), Some(uid), exp_reward @ 1..) = (
356                                    ev.tool,
357                                    maybe_uid,
358                                    items
359                                        .iter()
360                                        .filter_map(|item| {
361                                            item.item_definition_id().itemdef_id().and_then(|id| {
362                                                RESOURCE_EXPERIENCE_MANIFEST
363                                                    .read()
364                                                    .0
365                                                    .get(id)
366                                                    .copied()
367                                            })
368                                        })
369                                        .sum(),
370                                )
371                            {
372                                let skill_group = comp::SkillGroupKind::Weapon(tool);
373                                if let Some(level_outcome) =
374                                    skillset.add_experience(skill_group, exp_reward)
375                                {
376                                    outcome_emitter.emit(Outcome::SkillPointGain {
377                                        uid,
378                                        skill_tree: skill_group,
379                                        total_points: level_outcome,
380                                    });
381                                }
382                                outcome_emitter.emit(Outcome::ExpChange {
383                                    uid,
384                                    exp: exp_reward,
385                                    xp_pools: HashSet::from([skill_group]),
386                                });
387                            }
388
389                            let stage_ore_chance = || {
390                                let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.ore_gain);
391                                let skill_level = skillset
392                                    .skill_level(Skill::Pick(MiningSkill::OreGain))
393                                    .unwrap_or(0);
394
395                                chance_mod * f64::from(skill_level)
396                            };
397                            let stage_gem_chance = || {
398                                let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.gem_gain);
399                                let skill_level = skillset
400                                    .skill_level(Skill::Pick(MiningSkill::GemGain))
401                                    .unwrap_or(0);
402
403                                chance_mod * f64::from(skill_level)
404                            };
405
406                            // If the resource hasn't been fully broken, only drop certain resources
407                            // with a chance
408                            if !is_broken {
409                                items.retain(|item| {
410                                    rng.random_bool(
411                                        0.5 + item
412                                            .item_definition_id()
413                                            .itemdef_id()
414                                            .map(|id| {
415                                                if id.contains("mineral.ore.") {
416                                                    stage_ore_chance()
417                                                } else if id.contains("mineral.gem.") {
418                                                    stage_gem_chance()
419                                                } else {
420                                                    0.0
421                                                }
422                                            })
423                                            .unwrap_or(0.0),
424                                    )
425                                });
426                            }
427                        }
428                        for item in items {
429                            let loot_owner = maybe_uid
430                                .map(LootOwnerKind::Player)
431                                .map(|owner| comp::LootOwner::new(owner, false));
432                            create_item_drop_emitter.emit(CreateItemDropEvent {
433                                pos: comp::Pos(ev.pos.map(|e| e as f32) + Vec3::broadcast(0.5)),
434                                vel: comp::Vel(
435                                    Vec2::unit_x()
436                                        .rotated_z(rng.random::<f32>() * PI * 2.0)
437                                        .mul(4.0)
438                                        .with_z(rng.random_range(5.0..10.0)),
439                                ),
440                                ori: comp::Ori::from(Dir::random_2d(&mut rng)),
441                                item: comp::PickupItem::new(item, *program_time, false),
442                                loot_owner,
443                            });
444                        }
445                    }
446
447                    if damage.is_some() && !is_broken {
448                        block_change.set(ev.pos, block);
449                    } else {
450                        block_change.set(ev.pos, block.into_vacant());
451                    }
452                    outcome_emitter.emit(if is_broken {
453                        Outcome::BreakBlock {
454                            pos: ev.pos,
455                            tool: ev.tool,
456                            color: block.get_color(),
457                        }
458                    } else {
459                        Outcome::DamagedBlock {
460                            pos: ev.pos,
461                            stage_changed,
462                            tool: ev.tool,
463                        }
464                    });
465
466                    // Emit mining sound
467                    sound_event_emitter.emit(SoundEvent {
468                        sound: Sound::new(SoundKind::Mine, ev.pos.as_(), 20.0, time.0),
469                    });
470                }
471            }
472        }
473    }
474}
475
476impl ServerEvent for SoundEvent {
477    type SystemData<'a> = (
478        ReadExpect<'a, EventBus<Outcome>>,
479        WriteStorage<'a, comp::Agent>,
480        ReadStorage<'a, comp::Pos>,
481    );
482
483    fn handle(
484        events: impl ExactSizeIterator<Item = Self>,
485        (outcomes, mut agents, positions): Self::SystemData<'_>,
486    ) {
487        let mut outcome_emitter = outcomes.emitter();
488        for SoundEvent { sound } in events {
489            // TODO: Reduce the complexity of this problem by using spatial partitioning
490            // system
491            for (agent, agent_pos) in (&mut agents, &positions).join() {
492                // TODO: Use pathfinding for more dropoff around obstacles
493                let agent_dist_sqrd = agent_pos.0.distance_squared(sound.pos);
494                let sound_travel_dist_sqrd = (sound.vol * SOUND_TRAVEL_DIST_PER_VOLUME).powi(2);
495
496                let vol_dropoff = agent_dist_sqrd / sound_travel_dist_sqrd * sound.vol;
497                let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
498
499                let can_hear_sound = propagated_sound.vol > 0.00;
500                let should_hear_sound = agent_dist_sqrd < agent.psyche.listen_dist.powi(2);
501
502                if can_hear_sound && should_hear_sound {
503                    agent
504                        .inbox
505                        .push_back(AgentEvent::ServerSound(propagated_sound));
506                }
507            }
508
509            // Attempt to turn this sound into an outcome to be received by frontends.
510            if let Some(outcome) = match sound.kind {
511                SoundKind::Utterance(kind, body) => Some(Outcome::Utterance {
512                    kind,
513                    pos: sound.pos,
514                    body,
515                }),
516                _ => None,
517            } {
518                outcome_emitter.emit(outcome);
519            }
520        }
521    }
522}
523
524impl ServerEvent for CreateSpriteEvent {
525    type SystemData<'a> = (
526        WriteExpect<'a, BlockChange>,
527        WriteExpect<'a, ScheduledBlockChange>,
528        ReadExpect<'a, TerrainGrid>,
529        ReadExpect<'a, Time>,
530    );
531
532    fn handle(
533        events: impl ExactSizeIterator<Item = Self>,
534        (mut block_change, mut scheduled_block_change, terrain, time): Self::SystemData<'_>,
535    ) {
536        for ev in events {
537            if block_change.can_set_block(ev.pos) {
538                let block = terrain.get(ev.pos).ok().copied();
539                if block.is_some_and(|b| (*b).is_fluid()) {
540                    let old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty));
541                    let new_block = old_block.with_sprite(ev.sprite);
542                    block_change.set(ev.pos, new_block);
543                    // Remove sprite after del_timeout and offset if specified
544                    if let Some((timeout, del_offset)) = ev.del_timeout {
545                        use rand::Rng;
546                        let mut rng = rand::rng();
547                        let offset = rng.random_range(0.0..del_offset);
548                        let current_time: f64 = time.0;
549                        let replace_time = current_time + (timeout + offset) as f64;
550                        if old_block != new_block {
551                            scheduled_block_change.set(ev.pos, old_block, replace_time);
552                            scheduled_block_change.outcome_set(ev.pos, new_block, replace_time);
553                        }
554                    }
555                }
556            }
557        }
558    }
559}
560
561impl ServerEvent for ToggleSpriteLightEvent {
562    type SystemData<'a> = (
563        WriteExpect<'a, BlockChange>,
564        ReadExpect<'a, TerrainGrid>,
565        ReadStorage<'a, comp::Pos>,
566    );
567
568    fn handle(
569        events: impl ExactSizeIterator<Item = Self>,
570        (mut block_change, terrain, positions): Self::SystemData<'_>,
571    ) {
572        for ev in events.into_iter() {
573            if let Some(entity_pos) = positions.get(ev.entity)
574                && entity_pos.0.distance_squared(ev.pos.as_()) < MAX_INTERACT_RANGE.powi(2)
575                && block_change.can_set_block(ev.pos)
576                && let Some(new_block) = terrain
577                    .get(ev.pos)
578                    .ok()
579                    .and_then(|block| block.with_toggle_light(ev.enable))
580            {
581                block_change.set(ev.pos, new_block);
582                // TODO: Emit outcome
583            }
584        }
585    }
586}
587
588pub fn handle_tame_pet(server: &mut Server, ev: TamePetEvent) {
589    // TODO: Raise outcome to send to clients to play sound/render an indicator
590    // showing taming success?
591    tame_pet(server.state.ecs(), ev.pet_entity, ev.owner_entity);
592}