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