Skip to main content

veloren_server/events/
entity_manipulation.rs

1#[cfg(feature = "worldgen")]
2use crate::rtsim::RtSim;
3use crate::{
4    Server, Settings, SpawnPoint,
5    client::Client,
6    comp::{
7        BuffKind, BuffSource, PhysicsState,
8        agent::{Agent, AgentEvent, Sound, SoundKind},
9        loot_owner::LootOwner,
10        skillset::SkillGroupKind,
11    },
12    error,
13    events::entity_creation::handle_create_npc,
14    metrics::GameplayMetrics,
15    persistence::character_updater::CharacterUpdater,
16    pet::tame_pet,
17    state_ext::StateExt,
18    sys::terrain::{NpcData, SAFE_ZONE_RADIUS, SpawnEntityData},
19};
20#[cfg(feature = "worldgen")]
21use common::rtsim::{Actor, RtSimEntity};
22use common::{
23    CachedSpatialGrid, Damage, DamageKind, DamageSource, GroupTarget, RadiusEffect,
24    assets::{AssetExt, Ron},
25    combat::{
26        self, AttackSource, BASE_PARRIED_POISE_PUNISHMENT, CombatEffect, DamageContributor,
27        DeathEffects, StatEffect, StatEffectTarget,
28    },
29    comp::{
30        self, Alignment, Auras, BASE_ABILITY_LIMIT, Body, BuffCategory, BuffEffect, CharacterState,
31        Energy, Group, Hardcore, Health, HealthChange, Inventory, Object, PickupItem, Player,
32        Poise, PoiseChange, Pos, Presence, PresenceKind, ProjectileConstructor, SkillSet, Stats,
33        ability::Dodgeable,
34        aura::{self, EnteredAuras},
35        buff,
36        chat::{KillSource, KillType},
37        inventory::item::{AbilityMap, MaterialStatManifest},
38        item::flatten_counted_items,
39        loot_owner::{LootOwnerKind, ONWERSHIP_TIMEOUT_SLOW},
40        projectile::{ProjectileAttack, ProjectileConstructorKind},
41    },
42    consts::TELEPORTER_RADIUS,
43    event::{
44        AuraEvent, BonkEvent, BuffEvent, ChangeAbilityEvent, ChangeBodyEvent, ChangeStanceEvent,
45        ChatEvent, ComboChangeEvent, CreateItemDropEvent, CreateNpcEvent, CreateObjectEvent,
46        DeleteEvent, DestroyEvent, DownedEvent, EmitExt, Emitter, EnergyChangeEvent,
47        EntityAttackedHookEvent, EventBus, ExplosionEvent, HealthChangeEvent, HelpDownedEvent,
48        KillEvent, KnockbackEvent, LandOnGroundEvent, MakeAdminEvent, ParryHookEvent,
49        PermanentChange, PoiseChangeEvent, RegrowHeadEvent, RemoveLightEmitterEvent, RespawnEvent,
50        ShootEvent, SoundEvent, StartInteractionEvent, StartTeleportingEvent, TeleportToEvent,
51        TeleportToPositionEvent, TransformEvent, UpdateMapMarkerEvent,
52    },
53    event_emitters,
54    explosion::{ColorPreset, TerrainReplacementPreset},
55    generation::{EntityConfig, EntityInfo},
56    link::Is,
57    lottery::distribute_many,
58    mounting::{Mounting, Rider, VolumeRider},
59    npc::NPC_NAMES,
60    outcome::{HealthChangeInfo, Outcome},
61    resources::{EntitiesDiedLastTick, ProgramTime, Secs, Time},
62    spiral::Spiral2d,
63    states::utils::StageSection,
64    terrain::{Block, BlockKind, TerrainGrid},
65    trade::{TradeResult, Trades},
66    uid::{IdMaps, Uid},
67    util::Dir,
68    vol::ReadVol,
69};
70use common_net::{msg::ServerGeneral, sync::WorldSyncExt, synced_components::Heads};
71use common_state::{AreasContainer, BlockChange, NoDurabilityArea, ScheduledBlockChange};
72use hashbrown::HashSet;
73use rand::RngExt;
74use specs::{
75    DispatcherBuilder, Entities, Entity as EcsEntity, Entity, Join, LendJoin, Read, ReadExpect,
76    ReadStorage, SystemData, WorldExt, Write, WriteExpect, WriteStorage, shred,
77};
78#[cfg(feature = "worldgen")] use std::sync::Arc;
79use std::{borrow::Cow, collections::HashMap, f32::consts::PI, iter, time::Duration};
80use tracing::{debug, warn};
81use vek::{Rgb, Vec2, Vec3};
82#[cfg(feature = "worldgen")]
83use world::{IndexOwned, World};
84
85use super::{ServerEvent, event_dispatch, event_sys_name};
86
87pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
88    event_dispatch::<PoiseChangeEvent>(builder, &[]);
89    event_dispatch::<HealthChangeEvent>(builder, &[]);
90    event_dispatch::<KillEvent>(builder, &[]);
91    event_dispatch::<HelpDownedEvent>(builder, &[]);
92    event_dispatch::<DownedEvent>(builder, &[&event_sys_name::<HealthChangeEvent>()]);
93    event_dispatch::<KnockbackEvent>(builder, &[]);
94    event_dispatch::<DestroyEvent>(builder, &[&event_sys_name::<HealthChangeEvent>()]);
95    event_dispatch::<LandOnGroundEvent>(builder, &[]);
96    event_dispatch::<RespawnEvent>(builder, &[]);
97    event_dispatch::<ExplosionEvent>(builder, &[]);
98    event_dispatch::<BonkEvent>(builder, &[]);
99    event_dispatch::<AuraEvent>(builder, &[]);
100    event_dispatch::<BuffEvent>(builder, &[&event_sys_name::<DownedEvent>()]);
101    event_dispatch::<EnergyChangeEvent>(builder, &[]);
102    event_dispatch::<ComboChangeEvent>(builder, &[]);
103    event_dispatch::<ParryHookEvent>(builder, &[]);
104    event_dispatch::<TeleportToEvent>(builder, &[]);
105    event_dispatch::<EntityAttackedHookEvent>(builder, &[]);
106    event_dispatch::<ChangeAbilityEvent>(builder, &[]);
107    event_dispatch::<UpdateMapMarkerEvent>(builder, &[]);
108    event_dispatch::<MakeAdminEvent>(builder, &[]);
109    event_dispatch::<ChangeStanceEvent>(builder, &[]);
110    event_dispatch::<ChangeBodyEvent>(builder, &[]);
111    event_dispatch::<RemoveLightEmitterEvent>(builder, &[]);
112    event_dispatch::<TeleportToPositionEvent>(builder, &[]);
113    event_dispatch::<StartTeleportingEvent>(builder, &[]);
114    event_dispatch::<RegrowHeadEvent>(builder, &[]);
115}
116
117event_emitters! {
118    struct ReadExplosionEvents[ExplosionEmitters] {
119        health_change: HealthChangeEvent,
120        energy_change: EnergyChangeEvent,
121        poise_change: PoiseChangeEvent,
122        sound: SoundEvent,
123        parry_hook: ParryHookEvent,
124        knockback: KnockbackEvent,
125        entity_attack_hook: EntityAttackedHookEvent,
126        combo_change: ComboChangeEvent,
127        buff: BuffEvent,
128        bonk: BonkEvent,
129        change_body: ChangeBodyEvent,
130        outcome: Outcome,
131        stance: ChangeStanceEvent,
132        transform: TransformEvent,
133    }
134
135    struct ReadEntityAttackedHookEvents[EntityAttackedHookEmitters] {
136        buff: BuffEvent,
137        combo_change: ComboChangeEvent,
138        knockback: KnockbackEvent,
139        energy_change: EnergyChangeEvent,
140        transform: TransformEvent,
141        health_change: HealthChangeEvent,
142        poise_change: PoiseChangeEvent,
143    }
144
145    struct HealthChangeEvents[HealthChangeEmitters] {
146        destroy: DestroyEvent,
147        downed: DownedEvent,
148        outcome: Outcome,
149    }
150
151    struct DestroyEvents[DestroyEmitters] {
152        chat: ChatEvent,
153        create_item_drop: CreateItemDropEvent,
154        delete: DeleteEvent,
155        buff: BuffEvent,
156        transform: TransformEvent,
157        energy_change: EnergyChangeEvent,
158        health_change: HealthChangeEvent,
159        combo_change: ComboChangeEvent,
160        poise_change: PoiseChangeEvent,
161        knockback: KnockbackEvent,
162    }
163}
164
165pub fn handle_delete(server: &mut Server, DeleteEvent(entity): DeleteEvent) {
166    let _ = server
167        .state_mut()
168        .delete_entity_recorded(entity)
169        .map_err(|e| error!(?e, ?entity, "Failed to delete destroyed entity"));
170}
171
172#[derive(Hash, Eq, PartialEq)]
173enum DamageContrib {
174    Solo(EcsEntity),
175    Group(Group),
176    NotFound,
177}
178
179impl ServerEvent for PoiseChangeEvent {
180    type SystemData<'a> = (
181        Entities<'a>,
182        ReadStorage<'a, CharacterState>,
183        WriteStorage<'a, Poise>,
184    );
185
186    fn handle(
187        events: impl ExactSizeIterator<Item = Self>,
188        (entities, character_states, mut poises): Self::SystemData<'_>,
189    ) {
190        for ev in events {
191            if let Some((character_state, mut poise)) = (&character_states, &mut poises)
192                .lend_join()
193                .get(ev.entity, &entities)
194            {
195                // Entity is invincible to poise change during stunned character state.
196                if !matches!(character_state, CharacterState::Stunned(_)) {
197                    poise.change(ev.change);
198                }
199            }
200        }
201    }
202}
203
204#[cfg(feature = "worldgen")]
205pub fn entity_as_actor(
206    entity: Entity,
207    rtsim_entities: &ReadStorage<RtSimEntity>,
208    presences: &ReadStorage<Presence>,
209) -> Option<Actor> {
210    if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() {
211        Some(Actor::Npc(rtsim_entity))
212    } else if let Some(PresenceKind::Character(character)) = presences.get(entity).map(|p| p.kind) {
213        Some(Actor::Character(character))
214    } else {
215        None
216    }
217}
218
219#[derive(SystemData)]
220pub struct HealthChangeEventData<'a> {
221    entities: Entities<'a>,
222    msm: ReadExpect<'a, MaterialStatManifest>,
223    #[cfg(feature = "worldgen")]
224    rtsim: WriteExpect<'a, RtSim>,
225    events: HealthChangeEvents<'a>,
226    time: Read<'a, Time>,
227    #[cfg(feature = "worldgen")]
228    id_maps: Read<'a, IdMaps>,
229    #[cfg(feature = "worldgen")]
230    world: ReadExpect<'a, Arc<World>>,
231    #[cfg(feature = "worldgen")]
232    index: ReadExpect<'a, IndexOwned>,
233    positions: ReadStorage<'a, Pos>,
234    uids: ReadStorage<'a, Uid>,
235    #[cfg(feature = "worldgen")]
236    presences: ReadStorage<'a, Presence>,
237    #[cfg(feature = "worldgen")]
238    rtsim_entities: ReadStorage<'a, RtSimEntity>,
239    inventories: ReadStorage<'a, Inventory>,
240    agents: WriteStorage<'a, Agent>,
241    healths: WriteStorage<'a, Health>,
242    heads: WriteStorage<'a, Heads>,
243}
244
245impl ServerEvent for HealthChangeEvent {
246    type SystemData<'a> = HealthChangeEventData<'a>;
247
248    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
249        let mut emitters = data.events.get_emitters();
250        let mut rng = rand::rng();
251        for ev in events {
252            if let Some((mut health, inventory, pos, uid, heads)) = (
253                &mut data.healths,
254                data.inventories.maybe(),
255                data.positions.maybe(),
256                data.uids.maybe(),
257                (&mut data.heads).maybe(),
258            )
259                .lend_join()
260                .get(ev.entity, &data.entities)
261            {
262                // Skip damage if invincible.
263                if ev.change.amount < 0.0 &&
264                    // None indicates invincibility.
265                    combat::compute_protection(inventory, &data.msm).is_none()
266                {
267                    continue;
268                }
269
270                // If the change amount was not zero
271                let changed = health.change_by(ev.change);
272                if let Some(mut heads) = heads {
273                    // We want some hp to be left for a headless body, so we divide by (max amount
274                    // of heads + 2)
275                    let hp_per_head = health.maximum() / (heads.capacity() as f32 + 2.0);
276                    let target_heads = (health.current() / hp_per_head) as usize;
277                    if heads.amount() > 0 && ev.change.amount < 0.0 && heads.amount() > target_heads
278                    {
279                        for _ in target_heads..heads.amount() {
280                            if let Some(head) = heads.remove_one(&mut rng, *data.time) {
281                                if let Some(uid) = uid {
282                                    emitters.emit(Outcome::HeadLost { uid: *uid, head });
283                                }
284                            } else {
285                                break;
286                            }
287                        }
288                    }
289                }
290
291                #[cfg(feature = "worldgen")]
292                if changed {
293                    let entity_as_actor =
294                        |entity| entity_as_actor(entity, &data.rtsim_entities, &data.presences);
295                    if let Some(actor) = entity_as_actor(ev.entity) {
296                        let cause = ev
297                            .change
298                            .damage_by()
299                            .map(|by| by.uid())
300                            .and_then(|uid| data.id_maps.uid_entity(uid))
301                            .and_then(entity_as_actor);
302                        data.rtsim.hook_rtsim_actor_hp_change(
303                            &data.world,
304                            data.index.as_index_ref(),
305                            actor,
306                            cause,
307                            health.fraction(),
308                            ev.change.amount,
309                        );
310                    }
311                }
312
313                if let (Some(pos), Some(uid)) = (pos, uid)
314                    && changed
315                {
316                    emitters.emit(Outcome::HealthChange {
317                        pos: pos.0,
318                        info: HealthChangeInfo {
319                            amount: ev.change.amount,
320                            by: ev.change.by,
321                            target: *uid,
322                            cause: ev.change.cause,
323                            precise: ev.change.precise,
324                            instance: ev.change.instance,
325                        },
326                    });
327                }
328
329                if !health.is_dead && health.should_die() {
330                    if health.death_protection {
331                        emitters.emit(DownedEvent { entity: ev.entity });
332                    } else {
333                        emitters.emit(DestroyEvent {
334                            entity: ev.entity,
335                            cause: ev.change,
336                        });
337                    }
338                }
339            }
340
341            // This if statement filters out anything under 5 damage, for DOT ticks
342            // TODO: Find a better way to separate direct damage from DOT here
343            let damage = -ev.change.amount;
344            if damage > 5.0
345                && let Some(agent) = data.agents.get_mut(ev.entity)
346            {
347                agent.inbox.push_back(AgentEvent::Hurt);
348            }
349        }
350    }
351}
352
353impl ServerEvent for KillEvent {
354    type SystemData<'a> = WriteStorage<'a, comp::Health>;
355
356    fn handle(events: impl ExactSizeIterator<Item = Self>, mut healths: Self::SystemData<'_>) {
357        for ev in events {
358            if let Some(mut health) = healths.get_mut(ev.entity) {
359                health.kill();
360            }
361        }
362    }
363}
364
365#[derive(SystemData)]
366pub struct HelpDownedEventData<'a> {
367    id_maps: Read<'a, IdMaps>,
368    #[cfg(feature = "worldgen")]
369    rtsim: WriteExpect<'a, RtSim>,
370    #[cfg(feature = "worldgen")]
371    world: ReadExpect<'a, Arc<World>>,
372    #[cfg(feature = "worldgen")]
373    index: ReadExpect<'a, IndexOwned>,
374    #[cfg(feature = "worldgen")]
375    rtsim_entities: ReadStorage<'a, RtSimEntity>,
376    #[cfg(feature = "worldgen")]
377    presences: ReadStorage<'a, Presence>,
378    character_states: WriteStorage<'a, comp::CharacterState>,
379    healths: WriteStorage<'a, comp::Health>,
380}
381
382impl ServerEvent for HelpDownedEvent {
383    type SystemData<'a> = HelpDownedEventData<'a>;
384
385    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
386        for ev in events {
387            if let Some(entity) = data.id_maps.uid_entity(ev.target) {
388                if let Some(mut health) = data.healths.get_mut(entity) {
389                    health.refresh_death_protection();
390                }
391                if let Some(mut character_state) = data.character_states.get_mut(entity)
392                    && matches!(*character_state, comp::CharacterState::Crawl)
393                {
394                    *character_state = CharacterState::Idle(Default::default());
395                }
396
397                #[cfg(feature = "worldgen")]
398                let entity_as_actor =
399                    |entity| entity_as_actor(entity, &data.rtsim_entities, &data.presences);
400                #[cfg(feature = "worldgen")]
401                if let Some(actor) = entity_as_actor(entity) {
402                    let saver = ev
403                        .helper
404                        .and_then(|uid| data.id_maps.uid_entity(uid))
405                        .and_then(entity_as_actor);
406                    data.rtsim.hook_rtsim_actor_helped(
407                        &data.world,
408                        data.index.as_index_ref(),
409                        actor,
410                        saver,
411                    );
412                }
413            }
414        }
415    }
416}
417
418impl ServerEvent for DownedEvent {
419    type SystemData<'a> = (
420        Read<'a, EventBus<BuffEvent>>,
421        WriteStorage<'a, comp::CharacterState>,
422        WriteStorage<'a, comp::Health>,
423    );
424
425    fn handle(
426        events: impl ExactSizeIterator<Item = Self>,
427        (buff_event, mut character_states, mut healths): Self::SystemData<'_>,
428    ) {
429        let mut buff_emitter = buff_event.emitter();
430        for ev in events {
431            if let Some(mut health) = healths.get_mut(ev.entity) {
432                health.consume_death_protection()
433            }
434
435            if let Some(mut character_state) = character_states.get_mut(ev.entity) {
436                *character_state = CharacterState::Crawl;
437            }
438
439            // Remove buffs that don't persist when downed.
440            buff_emitter.emit(BuffEvent {
441                entity: ev.entity,
442                buff_change: comp::BuffChange::RemoveByCategory {
443                    all_required: vec![],
444                    any_required: vec![],
445                    none_required: vec![BuffCategory::PersistOnDowned],
446                },
447            });
448        }
449    }
450}
451
452impl ServerEvent for KnockbackEvent {
453    type SystemData<'a> = (
454        Entities<'a>,
455        ReadStorage<'a, Client>,
456        ReadStorage<'a, PhysicsState>,
457        ReadStorage<'a, comp::Mass>,
458        WriteStorage<'a, comp::Vel>,
459    );
460
461    fn handle(
462        events: impl ExactSizeIterator<Item = Self>,
463        (entities, clients, physic_states, mass, mut velocities): Self::SystemData<'_>,
464    ) {
465        for ev in events {
466            if let Some((physics, mass, vel, client)) = (
467                &physic_states,
468                mass.maybe(),
469                &mut velocities,
470                clients.maybe(),
471            )
472                .lend_join()
473                .get(ev.entity, &entities)
474            {
475                //Check if the entity is on a surface. If it is not, reduce knockback.
476                let mut impulse = ev.impulse
477                    * if physics.on_surface().is_some() {
478                        1.0
479                    } else {
480                        0.4
481                    };
482
483                // we go easy on the little ones (because they fly so far)
484                impulse /= mass.map_or(0.0, |m| m.0).max(40.0);
485
486                vel.0 += impulse;
487                if let Some(client) = client {
488                    client.send_fallible(ServerGeneral::Knockback(impulse));
489                }
490            }
491        }
492    }
493}
494
495fn handle_exp_gain(
496    exp_reward: f32,
497    inventory: &Inventory,
498    skill_set: &mut SkillSet,
499    uid: &Uid,
500    outcomes_emitter: &mut Emitter<Outcome>,
501) {
502    use comp::inventory::{item::ItemKind, slot::EquipSlot};
503
504    // Create hash set of xp pools to consider splitting xp amongst
505    let mut xp_pools = HashSet::<SkillGroupKind>::new();
506    // Insert general pool since it is always accessible
507    xp_pools.insert(SkillGroupKind::General);
508    // Closure to add xp pool corresponding to weapon type equipped in a particular
509    // EquipSlot
510    let mut add_tool_from_slot = |equip_slot| {
511        let tool_kind = inventory
512            .equipped(equip_slot)
513            .and_then(|i| match &*i.kind() {
514                ItemKind::Tool(tool) if tool.kind.gains_combat_xp() => Some(tool.kind),
515                _ => None,
516            });
517        if let Some(weapon) = tool_kind {
518            // Only adds to xp pools if entity has that skill group available
519            if skill_set.skill_group_accessible(SkillGroupKind::Weapon(weapon)) {
520                xp_pools.insert(SkillGroupKind::Weapon(weapon));
521            }
522        }
523    };
524    // Add weapons to xp pools considered
525    add_tool_from_slot(EquipSlot::ActiveMainhand);
526    add_tool_from_slot(EquipSlot::ActiveOffhand);
527    add_tool_from_slot(EquipSlot::InactiveMainhand);
528    add_tool_from_slot(EquipSlot::InactiveOffhand);
529    let num_pools = xp_pools.len() as f32;
530    for pool in xp_pools.iter() {
531        if let Some(level_outcome) =
532            skill_set.add_experience(*pool, (exp_reward / num_pools).ceil() as u32)
533        {
534            outcomes_emitter.emit(Outcome::SkillPointGain {
535                uid: *uid,
536                skill_tree: *pool,
537                total_points: level_outcome,
538            });
539        }
540    }
541    outcomes_emitter.emit(Outcome::ExpChange {
542        uid: *uid,
543        exp: exp_reward as u32,
544        xp_pools,
545    });
546}
547
548#[derive(SystemData)]
549pub struct DestroyEventData<'a> {
550    entities: Entities<'a>,
551    #[cfg(feature = "worldgen")]
552    rtsim: WriteExpect<'a, RtSim>,
553    id_maps: Read<'a, IdMaps>,
554    msm: ReadExpect<'a, MaterialStatManifest>,
555    ability_map: ReadExpect<'a, AbilityMap>,
556    time: Read<'a, Time>,
557    program_time: ReadExpect<'a, ProgramTime>,
558    #[cfg(feature = "worldgen")]
559    world: ReadExpect<'a, Arc<World>>,
560    #[cfg(feature = "worldgen")]
561    index: ReadExpect<'a, IndexOwned>,
562    areas_container: Read<'a, AreasContainer<NoDurabilityArea>>,
563    outcomes: Read<'a, EventBus<Outcome>>,
564    entities_died_last_tick: Write<'a, EntitiesDiedLastTick>,
565    melees: WriteStorage<'a, comp::Melee>,
566    beams: WriteStorage<'a, comp::Beam>,
567    skill_sets: WriteStorage<'a, SkillSet>,
568    inventories: WriteStorage<'a, Inventory>,
569    item_drops: WriteStorage<'a, comp::ItemDrops>,
570    velocities: WriteStorage<'a, comp::Vel>,
571    force_updates: WriteStorage<'a, comp::ForceUpdate>,
572    energies: WriteStorage<'a, Energy>,
573    character_states: WriteStorage<'a, CharacterState>,
574    death_effects: WriteStorage<'a, DeathEffects>,
575    players: ReadStorage<'a, Player>,
576    clients: ReadStorage<'a, Client>,
577    uids: ReadStorage<'a, Uid>,
578    positions: ReadStorage<'a, Pos>,
579    healths: WriteStorage<'a, Health>,
580    bodies: ReadStorage<'a, Body>,
581    poises: ReadStorage<'a, Poise>,
582    groups: ReadStorage<'a, Group>,
583    alignments: ReadStorage<'a, Alignment>,
584    stats: ReadStorage<'a, Stats>,
585    agents: ReadStorage<'a, Agent>,
586    #[cfg(feature = "worldgen")]
587    rtsim_entities: ReadStorage<'a, RtSimEntity>,
588    #[cfg(feature = "worldgen")]
589    presences: ReadStorage<'a, Presence>,
590    masses: ReadStorage<'a, comp::Mass>,
591    event_buses: DestroyEvents<'a>,
592    buffs: ReadStorage<'a, comp::Buffs>,
593    orientations: ReadStorage<'a, comp::Ori>,
594    combos: ReadStorage<'a, comp::Combo>,
595    gameplay_metrics: ReadExpect<'a, GameplayMetrics>,
596}
597
598/// Handle an entity dying. If it is a player, it will send a message to all
599/// other players. If the entity that killed it had stats, then give it exp for
600/// the kill. Experience given is equal to the level of the entity that was
601/// killed times 10.
602impl ServerEvent for DestroyEvent {
603    type SystemData<'a> = DestroyEventData<'a>;
604
605    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
606        let mut outcomes_emitter = data.outcomes.emitter();
607        let mut emitters = data.event_buses.get_emitters();
608        let mut rng = rand::rng();
609        data.entities_died_last_tick.0.clear();
610
611        for ev in events {
612            // TODO: Investigate duplicate `Destroy` events (but don't remove this).
613            // If the entity was already deleted, it can't be destroyed again.
614            if !data.entities.is_alive(ev.entity) {
615                continue;
616            }
617            let mut outcomes = data.outcomes.emitter();
618            if let Some(mut health) = data.healths.get_mut(ev.entity) {
619                if !health.is_dead {
620                    health.is_dead = true;
621
622                    if let Some(pos) = data.positions.get(ev.entity).copied() {
623                        data.entities_died_last_tick.0.push((ev.entity, pos));
624                    }
625
626                    if let Some(body) = data.bodies.get(ev.entity) {
627                        let npc_names = NPC_NAMES.read();
628                        let body_type = npc_names
629                            .get_species_meta(body)
630                            .map(|meta| meta.keyword.as_str())
631                            .unwrap_or("other");
632
633                        let weapon = match ev.cause.cause {
634                            Some(DamageSource::Attack(AttackSource::Melee)) => "melee",
635                            Some(DamageSource::Attack(AttackSource::Projectile)) => "projectile",
636                            Some(DamageSource::Attack(AttackSource::Beam)) => "beam",
637                            Some(DamageSource::Attack(AttackSource::GroundShockwave))
638                            | Some(DamageSource::Attack(AttackSource::AirShockwave))
639                            | Some(DamageSource::Attack(AttackSource::UndodgeableShockwave)) => {
640                                "shockwave"
641                            },
642                            Some(DamageSource::Attack(AttackSource::Explosion)) => "explosion",
643                            Some(DamageSource::Attack(AttackSource::Arc)) => "arc",
644                            Some(DamageSource::Attack(AttackSource::Pool)) => "pool",
645                            Some(DamageSource::Buff(_)) => "buff",
646                            Some(DamageSource::Other) => "other",
647                            Some(DamageSource::Falling) => "fall",
648                            None => "unknown",
649                        };
650
651                        data.gameplay_metrics
652                            .entity_kills_by_type
653                            .with_label_values(&[body_type, weapon])
654                            .inc();
655
656                        // quantize the kill locations to a grid size that won't
657                        // create a million prometheus series - in other words,
658                        // round down to the nearest 1000th block
659                        if let Some(pos) = data.positions.get(ev.entity) {
660                            const QUANTIZE: i32 = 1000;
661                            let wpos = pos.0.xy();
662
663                            let x = ((wpos.x.floor() as i32).div_euclid(QUANTIZE) * QUANTIZE)
664                                .to_string();
665                            let y = ((wpos.y.floor() as i32).div_euclid(QUANTIZE) * QUANTIZE)
666                                .to_string();
667
668                            data.gameplay_metrics
669                                .entity_kills_by_location
670                                .with_label_values(&[body_type, &x, &y])
671                                .inc();
672                        }
673                    }
674                } else {
675                    // Skip for entities that have already died
676                    continue;
677                }
678            }
679
680            // Remove components that should not persist across death
681            data.melees.remove(ev.entity);
682            data.beams.remove(ev.entity);
683
684            let get_attacker_name = |cause_of_death: KillType, by: Uid| -> KillSource {
685                // Get attacker entity
686                if let Some(char_entity) = data.id_maps.uid_entity(by) {
687                    // Check if attacker is another player or entity with stats (npc)
688                    if data.players.contains(char_entity) {
689                        KillSource::Player(by, cause_of_death)
690                    } else if let Some(stats) = data.stats.get(char_entity) {
691                        KillSource::NonPlayer(stats.name.clone(), cause_of_death)
692                    } else {
693                        KillSource::NonExistent(cause_of_death)
694                    }
695                } else {
696                    KillSource::NonExistent(cause_of_death)
697                }
698            };
699
700            // Push an outcome if entity is has a character state (entities that don't have
701            // one, we probably don't care about emitting death outcome)
702            if let Some((pos, _)) = (&data.positions, &data.character_states)
703                .lend_join()
704                .get(ev.entity, &data.entities)
705            {
706                outcomes_emitter.emit(Outcome::Death { pos: pos.0 });
707            }
708
709            let mut should_delete = true;
710
711            // Handle any effects on death
712            if let Some(killed_stats) = data.stats.get(ev.entity) {
713                let attacker_entity = ev.cause.by.and_then(|x| data.id_maps.uid_entity(x.uid()));
714                let attacker_dir = attacker_entity
715                    .and_then(|a| data.positions.get(a))
716                    .map(|p| p.0)
717                    .zip(data.positions.get(ev.entity).map(|p| p.0))
718                    .and_then(|(pos_a, pos_t)| Dir::from_unnormalized(pos_a - pos_t))
719                    .unwrap_or_default();
720                let damage_dealt = ev.cause.amount.abs();
721                let attack_source = ev.cause.cause.and_then(|c| {
722                    if let DamageSource::Attack(attack) = c {
723                        Some(attack)
724                    } else {
725                        None
726                    }
727                });
728
729                let mut death_effects = data
730                    .death_effects
731                    .remove(ev.entity)
732                    .map(|ef| ef.0.into_iter().map(Cow::Owned));
733
734                for effect in killed_stats
735                    .effects_on_death
736                    .iter()
737                    .map(Cow::Borrowed)
738                    .chain(death_effects.as_mut().map_or(
739                        &mut core::iter::empty() as &mut dyn Iterator<Item = Cow<StatEffect>>,
740                        |death_effects| death_effects as &mut dyn Iterator<Item = Cow<StatEffect>>,
741                    ))
742                {
743                    let dir = match effect.target {
744                        StatEffectTarget::Target => -attacker_dir,
745                        StatEffectTarget::Attacker => attacker_dir,
746                    };
747
748                    let dmg_contrib = data.uids.get(ev.entity).map(|uid| {
749                        DamageContributor::new(*uid, data.groups.get(ev.entity).copied())
750                    });
751
752                    let (effect_target, other_entity) = match effect.target {
753                        StatEffectTarget::Target => (ev.entity, attacker_entity),
754                        StatEffectTarget::Attacker => {
755                            if let Some(attacker) = attacker_entity {
756                                (attacker, Some(ev.entity))
757                            } else {
758                                continue;
759                            }
760                        },
761                    };
762
763                    let requirements_met = effect.requirements().all(|req| {
764                        req.requirement_met(
765                            (
766                                data.healths.get(effect_target),
767                                data.buffs.get(effect_target),
768                                data.character_states.get(effect_target),
769                                data.orientations.get(effect_target),
770                            ),
771                            (
772                                Some(ev.entity),
773                                data.energies.get(ev.entity),
774                                data.combos.get(ev.entity),
775                            ),
776                            ev.cause.by.map(|x| x.uid()),
777                            damage_dealt,
778                            &mut emitters,
779                            dir,
780                            attack_source,
781                            None,
782                        )
783                    });
784
785                    if requirements_met {
786                        let mut strength_modifier = 1.0;
787                        for modification in effect.modifications() {
788                            modification.apply_mod(
789                                data.positions.get(effect_target).map(|x| x.0),
790                                data.positions.get(ev.entity).map(|x| x.0),
791                                &mut strength_modifier,
792                            )
793                        }
794                        let strength_modifier = strength_modifier;
795
796                        match &effect.effect {
797                            CombatEffect::Knockback(kb) => {
798                                let char_state = data.character_states.get(effect_target);
799                                let impulse = kb.calculate_impulse(
800                                    dir,
801                                    char_state,
802                                    attacker_entity.and_then(|ae| data.stats.get(ae)),
803                                ) * strength_modifier;
804                                if !impulse.is_approx_zero() {
805                                    emitters.emit(KnockbackEvent {
806                                        entity: effect_target,
807                                        impulse,
808                                    });
809                                }
810                            },
811                            CombatEffect::EnergyReward(ec) => {
812                                emitters.emit(EnergyChangeEvent {
813                                    entity: effect_target,
814                                    change: ec
815                                        * combat::compute_energy_reward_mod(
816                                            data.inventories.get(effect_target),
817                                            &data.msm,
818                                        )
819                                        * strength_modifier
820                                        * data
821                                            .stats
822                                            .get(effect_target)
823                                            .map_or(1.0, |s| s.energy_reward_modifier),
824                                    reset_rate: false,
825                                });
826                            },
827                            CombatEffect::Buff(b) => {
828                                if rng.random::<f32>() < b.chance {
829                                    emitters.emit(BuffEvent {
830                                        entity: effect_target,
831                                        buff_change: buff::BuffChange::Add(b.to_buff(
832                                            *data.time,
833                                            (
834                                                data.uids.get(ev.entity).copied(),
835                                                data.masses.get(ev.entity),
836                                                None,
837                                            ),
838                                            (
839                                                data.stats.get(effect_target),
840                                                data.masses.get(effect_target),
841                                            ),
842                                            damage_dealt,
843                                            strength_modifier,
844                                        )),
845                                    });
846                                }
847                            },
848                            CombatEffect::Lifesteal(l) => {
849                                let change = HealthChange {
850                                    amount: damage_dealt * l * strength_modifier,
851                                    by: dmg_contrib,
852                                    cause: None,
853                                    time: *data.time,
854                                    precise: false,
855                                    instance: rand::random(),
856                                };
857                                if change.amount.abs() > Health::HEALTH_EPSILON {
858                                    emitters.emit(HealthChangeEvent {
859                                        entity: effect_target,
860                                        change,
861                                    });
862                                }
863                            },
864                            CombatEffect::Poise(p) => {
865                                let change = -Poise::apply_poise_reduction(
866                                    *p,
867                                    data.inventories.get(effect_target),
868                                    &data.msm,
869                                    data.character_states.get(effect_target),
870                                    data.stats.get(effect_target),
871                                ) * strength_modifier
872                                    * data
873                                        .stats
874                                        .get(ev.entity)
875                                        .map_or(1.0, |s| s.poise_damage_modifier);
876                                if change.abs() > Poise::POISE_EPSILON {
877                                    let poise_change = PoiseChange {
878                                        amount: change,
879                                        impulse: *dir,
880                                        by: dmg_contrib,
881                                        cause: None,
882                                        time: *data.time,
883                                    };
884                                    emitters.emit(PoiseChangeEvent {
885                                        entity: effect_target,
886                                        change: poise_change,
887                                    });
888                                }
889                            },
890                            CombatEffect::Heal(h) => {
891                                let change = HealthChange {
892                                    amount: *h * strength_modifier,
893                                    by: dmg_contrib,
894                                    cause: None,
895                                    time: *data.time,
896                                    precise: false,
897                                    instance: rand::random(),
898                                };
899                                if change.amount.abs() > Health::HEALTH_EPSILON {
900                                    emitters.emit(HealthChangeEvent {
901                                        entity: effect_target,
902                                        change,
903                                    });
904                                }
905                            },
906                            CombatEffect::Combo(c) => {
907                                emitters.emit(ComboChangeEvent {
908                                    entity: effect_target,
909                                    change: (*c as f32 * strength_modifier).ceil() as i32,
910                                });
911                            },
912                            CombatEffect::StageVulnerable(damage, section) => {
913                                if data
914                                    .character_states
915                                    .get(effect_target)
916                                    .is_some_and(|cs| cs.stage_section() == Some(*section))
917                                {
918                                    let change = HealthChange {
919                                        amount: -damage_dealt * damage * strength_modifier,
920                                        by: dmg_contrib,
921                                        cause: Some(DamageSource::Other),
922                                        time: *data.time,
923                                        precise: false,
924                                        instance: rand::random(),
925                                    };
926                                    emitters.emit(HealthChangeEvent {
927                                        entity: effect_target,
928                                        change,
929                                    });
930                                }
931                            },
932                            CombatEffect::RefreshBuff(chance, b) => {
933                                if rng.random::<f32>() < *chance {
934                                    emitters.emit(BuffEvent {
935                                        entity: effect_target,
936                                        buff_change: buff::BuffChange::Refresh(*b),
937                                    });
938                                }
939                            },
940                            CombatEffect::BuffsVulnerable(damage, buff) => {
941                                if data
942                                    .buffs
943                                    .get(effect_target)
944                                    .is_some_and(|b| b.contains(*buff))
945                                {
946                                    let change = HealthChange {
947                                        amount: -damage_dealt * damage * strength_modifier,
948                                        by: dmg_contrib,
949                                        cause: Some(DamageSource::Other),
950                                        time: *data.time,
951                                        precise: false,
952                                        instance: rand::random(),
953                                    };
954                                    emitters.emit(HealthChangeEvent {
955                                        entity: effect_target,
956                                        change,
957                                    });
958                                }
959                            },
960                            CombatEffect::StunnedVulnerable(damage) => {
961                                if data
962                                    .character_states
963                                    .get(effect_target)
964                                    .is_some_and(|cs| cs.is_stunned())
965                                {
966                                    let change = HealthChange {
967                                        amount: -damage_dealt * damage * strength_modifier,
968                                        by: dmg_contrib,
969                                        cause: Some(DamageSource::Other),
970                                        time: *data.time,
971                                        precise: false,
972                                        instance: rand::random(),
973                                    };
974                                    emitters.emit(HealthChangeEvent {
975                                        entity: effect_target,
976                                        change,
977                                    });
978                                }
979                            },
980                            CombatEffect::SelfBuff(b) => {
981                                if rng.random::<f32>() < b.chance {
982                                    emitters.emit(BuffEvent {
983                                        entity: effect_target,
984                                        buff_change: buff::BuffChange::Add(b.to_self_buff(
985                                            *data.time,
986                                            (
987                                                data.uids.get(effect_target).copied(),
988                                                data.stats.get(effect_target),
989                                                data.masses.get(effect_target),
990                                                None,
991                                            ),
992                                            damage_dealt,
993                                            strength_modifier,
994                                        )),
995                                    });
996                                }
997                            },
998                            CombatEffect::Energy(e) => {
999                                emitters.emit(EnergyChangeEvent {
1000                                    entity: effect_target,
1001                                    change: *e * strength_modifier,
1002                                    reset_rate: true,
1003                                });
1004                            },
1005                            CombatEffect::Transform {
1006                                entity_spec,
1007                                allow_players,
1008                            } => {
1009                                if (data.players.get(effect_target).is_none() || *allow_players)
1010                                    && let Some(tgt_uid) = data.uids.get(effect_target)
1011                                {
1012                                    if matches!(effect.target, StatEffectTarget::Target) {
1013                                        should_delete = false;
1014                                    }
1015                                    emitters.emit(TransformEvent {
1016                                        target_entity: *tgt_uid,
1017                                        entity_info: {
1018                                            let Ok(entity_config) = Ron::<EntityConfig>::load(
1019                                                entity_spec,
1020                                            )
1021                                            .inspect_err(|error| {
1022                                                error!(
1023                                                    ?entity_spec,
1024                                                    ?error,
1025                                                    "Could not load entity configuration for \
1026                                                     death effect"
1027                                                )
1028                                            }) else {
1029                                                continue;
1030                                            };
1031
1032                                            EntityInfo::at(
1033                                                data.positions
1034                                                    .get(effect_target)
1035                                                    .map(|p| p.0)
1036                                                    .unwrap_or_default(),
1037                                            )
1038                                            .with_entity_config(
1039                                                entity_config.read().clone().into_inner(),
1040                                                Some(entity_spec),
1041                                                &mut rng,
1042                                                None,
1043                                            )
1044                                        },
1045                                        allow_players: *allow_players,
1046                                        delete_on_failure: false,
1047                                    });
1048                                }
1049                            },
1050                            CombatEffect::DebuffsVulnerable {
1051                                mult,
1052                                scaling,
1053                                filter_attacker,
1054                                filter_weapon,
1055                            } => {
1056                                if let Some(buffs) = data.buffs.get(effect_target) {
1057                                    let num_debuffs = buffs.iter_active().flatten().filter(|b| {
1058                                        let debuff_filter = matches!(b.kind.differentiate(), buff::BuffDescriptor::SimpleNegative);
1059                                        let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == other_entity.and_then(|e| data.uids.get(e)).copied());
1060                                        let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
1061                                        debuff_filter && attacker_filter && weapon_filter
1062                                    }).count();
1063                                    if num_debuffs > 0 {
1064                                        let change = HealthChange {
1065                                            amount: -damage_dealt
1066                                                * scaling.factor(num_debuffs as f32, 1.0)
1067                                                * mult
1068                                                * strength_modifier,
1069                                            by: dmg_contrib,
1070                                            cause: Some(DamageSource::Other),
1071                                            time: *data.time,
1072                                            precise: false,
1073                                            instance: rand::random(),
1074                                        };
1075                                        emitters.emit(HealthChangeEvent {
1076                                            entity: effect_target,
1077                                            change,
1078                                        });
1079                                    }
1080                                }
1081                            },
1082                        }
1083                    }
1084                }
1085            }
1086
1087            // Chat message
1088            // If it was a player that died
1089            if let Some((uid, _player)) = (&data.uids, &data.players)
1090                .lend_join()
1091                .get(ev.entity, &data.entities)
1092            {
1093                let kill_source = match (ev.cause.cause, ev.cause.by.map(|x| x.uid())) {
1094                    (Some(DamageSource::Attack(AttackSource::Melee)), Some(by)) => {
1095                        get_attacker_name(KillType::Melee, by)
1096                    },
1097                    (Some(DamageSource::Attack(AttackSource::Projectile)), Some(by)) => {
1098                        get_attacker_name(KillType::Projectile, by)
1099                    },
1100                    (Some(DamageSource::Attack(AttackSource::Explosion)), Some(by)) => {
1101                        get_attacker_name(KillType::Explosion, by)
1102                    },
1103                    (
1104                        Some(DamageSource::Attack(AttackSource::Beam | AttackSource::Arc)),
1105                        Some(by),
1106                    ) => get_attacker_name(KillType::Energy, by),
1107                    (Some(DamageSource::Buff(buff_kind)), by) => {
1108                        if let Some(by) = by {
1109                            get_attacker_name(KillType::Buff(buff_kind), by)
1110                        } else {
1111                            KillSource::NonExistent(KillType::Buff(buff_kind))
1112                        }
1113                    },
1114                    (Some(DamageSource::Other), Some(by)) => get_attacker_name(KillType::Other, by),
1115                    (Some(DamageSource::Falling), _) => KillSource::FallDamage,
1116                    // HealthSource::Suicide => KillSource::Suicide,
1117                    _ => KillSource::Other,
1118                };
1119
1120                emitters.emit(ChatEvent {
1121                    msg: comp::UnresolvedChatMsg::death(kill_source, *uid),
1122                    from_client: false,
1123                });
1124            }
1125
1126            let mut exp_awards = Vec::<(Entity, f32, Option<Group>)>::new();
1127            // Award EXP to damage contributors
1128            //
1129            // NOTE: Debug logging is disabled by default for this module - to enable it add
1130            // veloren_server::events::entity_manipulation=debug to RUST_LOG
1131            'xp: {
1132                let Some((
1133                    entity_skill_set,
1134                    entity_health,
1135                    entity_energy,
1136                    entity_inventory,
1137                    entity_body,
1138                    entity_poise,
1139                    entity_pos,
1140                )) = (
1141                    &data.skill_sets,
1142                    &data.healths,
1143                    &data.energies,
1144                    &data.inventories,
1145                    &data.bodies,
1146                    &data.poises,
1147                    &data.positions,
1148                )
1149                    .lend_join()
1150                    .get(ev.entity, &data.entities)
1151                else {
1152                    break 'xp;
1153                };
1154
1155                // Calculate the total EXP award for the kill
1156                let exp_reward = combat::combat_rating(
1157                    entity_inventory,
1158                    entity_health,
1159                    entity_energy,
1160                    entity_poise,
1161                    entity_skill_set,
1162                    *entity_body,
1163                    &data.msm,
1164                ) * 20.0;
1165
1166                let mut damage_contributors = HashMap::<DamageContrib, (u64, f32)>::new();
1167                for (damage_contributor, damage) in entity_health.damage_contributions() {
1168                    match damage_contributor {
1169                        DamageContributor::Solo(uid) => {
1170                            if let Some(attacker) = data.id_maps.uid_entity(*uid) {
1171                                damage_contributors
1172                                    .insert(DamageContrib::Solo(attacker), (*damage, 0.0));
1173                            } else {
1174                                // An entity who was not in a group contributed damage but is now
1175                                // either dead or offline. Add a
1176                                // placeholder to ensure that the contributor's
1177                                // exp is discarded, not distributed between
1178                                // the other contributors
1179                                damage_contributors.insert(DamageContrib::NotFound, (*damage, 0.0));
1180                            }
1181                        },
1182                        DamageContributor::Group {
1183                            entity_uid: _,
1184                            group,
1185                        } => {
1186                            // Damage made by entities who were in a group at the time of attack is
1187                            // attributed to their group rather than themselves. This allows for all
1188                            // members of a group to receive EXP, not just the damage dealers.
1189                            let entry = damage_contributors
1190                                .entry(DamageContrib::Group(*group))
1191                                .or_insert((0, 0.0));
1192                            entry.0 += damage;
1193                        },
1194                    }
1195                }
1196
1197                // Calculate the percentage of total damage that each DamageContributor
1198                // contributed
1199                let total_damage: f64 = damage_contributors
1200                    .values()
1201                    .map(|(damage, _)| *damage as f64)
1202                    .sum();
1203                damage_contributors
1204                    .iter_mut()
1205                    .for_each(|(_, (damage, percentage))| {
1206                        *percentage = (*damage as f64 / total_damage) as f32
1207                    });
1208
1209                let destroyed_group = data.groups.get(ev.entity);
1210
1211                let within_range = |attacker_pos: &Pos| {
1212                    // Maximum distance that an attacker must be from an entity at the time of its
1213                    // death to receive EXP for the kill
1214                    const MAX_EXP_DIST: f32 = 150.0;
1215                    entity_pos.0.distance_squared(attacker_pos.0) < MAX_EXP_DIST.powi(2)
1216                };
1217
1218                let is_pvp_kill = |attacker: Entity| {
1219                    data.players.contains(ev.entity) && data.players.contains(attacker)
1220                };
1221
1222                // Iterate through all contributors of damage for the killed entity, calculating
1223                // how much EXP each contributor should be awarded based on their
1224                // percentage of damage contribution
1225                exp_awards = damage_contributors.iter().filter_map(|(damage_contributor, (_, damage_percent))| {
1226                let contributor_exp = exp_reward * damage_percent;
1227                match damage_contributor {
1228                    DamageContrib::Solo(attacker) => {
1229                        // No exp for self kills or PvP
1230                        if *attacker == ev.entity || is_pvp_kill(*attacker) { return None; }
1231
1232                        // Only give EXP to the attacker if they are within EXP range of the killed entity
1233                        data.positions.get(*attacker).and_then(|attacker_pos| {
1234                            if within_range(attacker_pos) {
1235                                debug!("Awarding {} exp to individual {:?} who contributed {}% damage to the kill of {:?}", contributor_exp, attacker, *damage_percent * 100.0, ev.entity);
1236                                Some(iter::once((*attacker, contributor_exp, None)).collect())
1237                            } else {
1238                                None
1239                            }
1240                        })
1241                    },
1242                    DamageContrib::Group(group) => {
1243                        // Don't give EXP to members in the destroyed entity's group
1244                        if destroyed_group == Some(group) { return None; }
1245
1246                        // Only give EXP to members of the group that are within EXP range of the killed entity and aren't a pet
1247                        let members_in_range = (
1248                            &data.entities,
1249                            &data.groups,
1250                            &data.positions,
1251                            data.alignments.maybe(),
1252                            &data.uids,
1253                        )
1254                            .join()
1255                            .filter_map(|(member_entity, member_group, member_pos, alignment, uid)| {
1256                                if *member_group == *group && within_range(member_pos) && !is_pvp_kill(member_entity) && !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
1257                                    Some(member_entity)
1258                                } else {
1259                                    None
1260                                }
1261                            })
1262                            .collect::<Vec<_>>();
1263
1264                        if members_in_range.is_empty() { return None; }
1265
1266                        // Divide EXP reward by square root of number of people in group for group EXP scaling
1267                        let exp_per_member = contributor_exp / (members_in_range.len() as f32).sqrt();
1268
1269                        debug!("Awarding {} exp per member of group ID {:?} with {} members which contributed {}% damage to the kill of {:?}", exp_per_member, group, members_in_range.len(), *damage_percent * 100.0, ev.entity);
1270                        Some(members_in_range.into_iter().map(|entity| (entity, exp_per_member, Some(*group))).collect::<Vec<(Entity, f32, Option<Group>)>>())
1271                    },
1272                    DamageContrib::NotFound => {
1273                        // Discard exp for dead/offline individual damage contributors
1274                        None
1275                    }
1276                }
1277            }).flatten().collect::<Vec<(Entity, f32, Option<Group>)>>();
1278
1279                exp_awards.iter().for_each(|(attacker, exp_reward, _)| {
1280                    // Process the calculated EXP rewards
1281                    if let Some((mut attacker_skill_set, attacker_uid, attacker_inventory)) =
1282                        (&mut data.skill_sets, &data.uids, &data.inventories)
1283                            .lend_join()
1284                            .get(*attacker, &data.entities)
1285                    {
1286                        handle_exp_gain(
1287                            *exp_reward,
1288                            attacker_inventory,
1289                            &mut attacker_skill_set,
1290                            attacker_uid,
1291                            &mut outcomes,
1292                        );
1293                    }
1294                });
1295            };
1296
1297            should_delete &= if data.clients.contains(ev.entity) {
1298                if let Some(vel) = data.velocities.get_mut(ev.entity) {
1299                    vel.0 = Vec3::zero();
1300                }
1301                if let Some(force_update) = data.force_updates.get_mut(ev.entity) {
1302                    force_update.update();
1303                }
1304                if let Some(mut energy) = data.energies.get_mut(ev.entity) {
1305                    energy.refresh();
1306                }
1307                if let Some(mut character_state) = data.character_states.get_mut(ev.entity) {
1308                    *character_state = CharacterState::default();
1309                }
1310
1311                false
1312            } else {
1313                if let Some((_agent, pos, alignment, vel)) = (
1314                    &data.agents,
1315                    &data.positions,
1316                    data.alignments.maybe(),
1317                    data.velocities.maybe(),
1318                )
1319                    .lend_join()
1320                    .get(ev.entity, &data.entities)
1321                {
1322                    // Only drop loot if entity has agency (not a player),
1323                    // and if it is not owned by another entity (not a pet)
1324                    if !matches!(alignment, Some(Alignment::Owned(_)))
1325                        && let Some(items) = data
1326                            .item_drops
1327                            .remove(ev.entity)
1328                            .map(|comp::ItemDrops(item)| item)
1329                    {
1330                        // Remove entries where zero exp was awarded - this happens because some
1331                        // entities like Object bodies don't give EXP.
1332                        let mut item_receivers = HashMap::new();
1333                        for (entity, exp, group) in exp_awards {
1334                            if exp >= f32::EPSILON {
1335                                let loot_owner = if let Some(group) = group {
1336                                    Some(LootOwnerKind::Group(group))
1337                                } else {
1338                                    let uid = data.bodies.get(entity).and_then(|body| {
1339                                        // Only humanoids are awarded loot ownership - if the winner
1340                                        // was a non-humanoid NPC the loot will be free-for-all
1341                                        if matches!(body, Body::Humanoid(_)) {
1342                                            data.uids.get(entity).copied()
1343                                        } else {
1344                                            None
1345                                        }
1346                                    });
1347
1348                                    uid.map(LootOwnerKind::Player)
1349                                };
1350
1351                                *item_receivers.entry(loot_owner).or_insert(0.0) += exp;
1352                            }
1353                        }
1354
1355                        let mut item_offset_spiral =
1356                            Spiral2d::new().map(|offset| offset.as_::<f32>() * 0.5);
1357
1358                        let mut rng = rand::rng();
1359                        let mut spawn_item = |item, loot_owner| {
1360                            let offset = item_offset_spiral.next().unwrap_or_default();
1361                            emitters.emit(CreateItemDropEvent {
1362                                pos: Pos(pos.0 + Vec3::unit_z() * 0.25 + offset),
1363                                vel: vel.copied().unwrap_or(comp::Vel(Vec3::zero())),
1364                                ori: comp::Ori::from(Dir::random_2d(&mut rng)),
1365                                item: PickupItem::new(item, *data.program_time, false),
1366                                loot_owner: if let Some(loot_owner) = loot_owner {
1367                                    debug!(
1368                                        "Assigned UID {loot_owner:?} as the winner for the loot \
1369                                         drop"
1370                                    );
1371                                    Some(LootOwner::new(loot_owner, false, ONWERSHIP_TIMEOUT_SLOW))
1372                                } else {
1373                                    debug!("No loot owner");
1374                                    None
1375                                },
1376                            })
1377                        };
1378
1379                        if item_receivers.is_empty() {
1380                            debug!("No item receivers");
1381                            for item in flatten_counted_items(&items, &data.ability_map, &data.msm)
1382                            {
1383                                spawn_item(item, None)
1384                            }
1385                        } else {
1386                            let mut rng = rand::rng();
1387                            distribute_many(
1388                                item_receivers
1389                                    .iter()
1390                                    .map(|(loot_owner, weight)| (*weight, *loot_owner)),
1391                                &mut rng,
1392                                &items,
1393                                |(amount, _)| *amount,
1394                                |(_, item), loot_owner, count| {
1395                                    for item in
1396                                        item.stacked_duplicates(&data.ability_map, &data.msm, count)
1397                                    {
1398                                        spawn_item(item, loot_owner)
1399                                    }
1400                                },
1401                            );
1402                        }
1403                    }
1404                }
1405                true
1406            };
1407            if !should_delete {
1408                let resists_durability =
1409                    data.positions
1410                        .get(ev.entity)
1411                        .cloned()
1412                        .is_some_and(|our_pos| {
1413                            let our_pos = our_pos.0.map(|i| i as i32);
1414
1415                            data.areas_container
1416                                .areas()
1417                                .iter()
1418                                .any(|(_, area)| area.contains_point(our_pos))
1419                        });
1420
1421                // Modify durability on all equipped items
1422                if !resists_durability
1423                    && let Some(mut inventory) = data.inventories.get_mut(ev.entity)
1424                {
1425                    inventory.damage_items(&data.ability_map, &data.msm, *data.time);
1426                }
1427            }
1428
1429            #[cfg(feature = "worldgen")]
1430            let entity_as_actor =
1431                |entity| entity_as_actor(entity, &data.rtsim_entities, &data.presences);
1432
1433            #[cfg(feature = "worldgen")]
1434            if let Some(actor) = entity_as_actor(ev.entity)
1435                // Skip the death hook for rtsim entities if they aren't deleted, otherwise
1436                // we'll end up with rtsim respawning an entity that wasn't actually
1437                // removed, producing 2 entities having the same RtsimEntityId.
1438                // Additionally, the death of a player should trigger an event.
1439                && (matches!(actor, Actor::Character(_)) || should_delete)
1440            {
1441                data.rtsim.hook_rtsim_actor_death(
1442                    &data.world,
1443                    data.index.as_index_ref(),
1444                    actor,
1445                    data.positions.get(ev.entity).map(|p| p.0),
1446                    ev.cause
1447                        .by
1448                        .as_ref()
1449                        .and_then(
1450                            |(DamageContributor::Solo(entity_uid)
1451                             | DamageContributor::Group { entity_uid, .. })| {
1452                                data.id_maps.uid_entity(*entity_uid)
1453                            },
1454                        )
1455                        .and_then(entity_as_actor),
1456                );
1457            }
1458
1459            if should_delete {
1460                emitters.emit(DeleteEvent(ev.entity));
1461            }
1462        }
1463    }
1464}
1465
1466impl ServerEvent for LandOnGroundEvent {
1467    type SystemData<'a> = (
1468        Read<'a, Time>,
1469        ReadExpect<'a, MaterialStatManifest>,
1470        Read<'a, EventBus<HealthChangeEvent>>,
1471        Read<'a, EventBus<PoiseChangeEvent>>,
1472        ReadStorage<'a, PhysicsState>,
1473        ReadStorage<'a, CharacterState>,
1474        ReadStorage<'a, comp::Mass>,
1475        ReadStorage<'a, Inventory>,
1476        ReadStorage<'a, Stats>,
1477    );
1478
1479    fn handle(
1480        events: impl ExactSizeIterator<Item = Self>,
1481        (
1482            time,
1483            msm,
1484            health_change_events,
1485            poise_change_events,
1486            physic_states,
1487            character_states,
1488            masses,
1489            inventories,
1490            stats,
1491        ): Self::SystemData<'_>,
1492    ) {
1493        let mut health_change_emitter = health_change_events.emitter();
1494        let mut poise_change_emitter = poise_change_events.emitter();
1495        for ev in events {
1496            // HACK: Certain ability movements currently take us above the fall damage
1497            // threshold in the horizontal axis. This factor dampens velocity in the
1498            // horizontal axis when applying fall damage.
1499            let horizontal_damp = 0.5
1500                + ev.vel
1501                    .try_normalized()
1502                    .unwrap_or_default()
1503                    .dot(Vec3::unit_z())
1504                    .abs()
1505                    * 0.5;
1506
1507            let relative_vel = ev.vel.dot(-ev.surface_normal) * horizontal_damp;
1508            // The second part of this if statement disables all fall damage when in the
1509            // water. This was added as a *temporary* fix a bug that causes you to take
1510            // fall damage while swimming downwards. FIXME: Fix the actual bug and
1511            // remove the following relevant part of the if statement.
1512            let falldmg_threshold = 30.0;
1513            if relative_vel >= falldmg_threshold
1514                && physic_states
1515                    .get(ev.entity)
1516                    .is_none_or(|ps| ps.in_liquid().is_none())
1517            {
1518                let reduced_vel =
1519                    if let Some(CharacterState::DiveMelee(c)) = character_states.get(ev.entity) {
1520                        (relative_vel + c.static_data.vertical_speed).min(0.0)
1521                    } else {
1522                        relative_vel
1523                    };
1524
1525                let mass = masses.get(ev.entity).copied().unwrap_or_default();
1526                let excess_energy = mass.0 * (reduced_vel - falldmg_threshold).powi(2) / 2.0;
1527                let falldmg = excess_energy / 1000.0;
1528
1529                // Emit health change
1530                let damage = Damage {
1531                    kind: DamageKind::Crushing,
1532                    value: falldmg,
1533                };
1534                let damage_reduction = Damage::compute_damage_reduction(
1535                    Some(damage),
1536                    inventories.get(ev.entity),
1537                    stats.get(ev.entity),
1538                    &msm,
1539                );
1540                let change = damage.calculate_health_change(
1541                    damage_reduction,
1542                    0.0,
1543                    None,
1544                    None,
1545                    0.0,
1546                    1.0,
1547                    *time,
1548                    rand::random(),
1549                    DamageSource::Falling,
1550                );
1551
1552                health_change_emitter.emit(HealthChangeEvent {
1553                    entity: ev.entity,
1554                    change,
1555                });
1556
1557                // Emit poise change
1558                let poise_damage = -(mass.0 * reduced_vel.powi(2) / 1500.0);
1559                let poise_change = Poise::apply_poise_reduction(
1560                    poise_damage,
1561                    inventories.get(ev.entity),
1562                    &msm,
1563                    character_states.get(ev.entity),
1564                    stats.get(ev.entity),
1565                );
1566                let poise_change = comp::PoiseChange {
1567                    amount: poise_change,
1568                    impulse: Vec3::unit_z(),
1569                    by: None,
1570                    cause: None,
1571                    time: *time,
1572                };
1573                poise_change_emitter.emit(PoiseChangeEvent {
1574                    entity: ev.entity,
1575                    change: poise_change,
1576                });
1577            }
1578        }
1579    }
1580}
1581
1582impl ServerEvent for RespawnEvent {
1583    type SystemData<'a> = (
1584        Read<'a, SpawnPoint>,
1585        WriteStorage<'a, Health>,
1586        WriteStorage<'a, comp::Combo>,
1587        WriteStorage<'a, Pos>,
1588        WriteStorage<'a, comp::PhysicsState>,
1589        WriteStorage<'a, comp::ForceUpdate>,
1590        WriteStorage<'a, Heads>,
1591        ReadStorage<'a, Client>,
1592        ReadStorage<'a, Hardcore>,
1593        ReadStorage<'a, comp::Waypoint>,
1594    );
1595
1596    fn handle(
1597        events: impl ExactSizeIterator<Item = Self>,
1598        (
1599            spawn_point,
1600            mut healths,
1601            mut combos,
1602            mut positions,
1603            mut physic_states,
1604            mut force_updates,
1605            mut heads,
1606            clients,
1607            hardcore,
1608            waypoints,
1609        ): Self::SystemData<'_>,
1610    ) {
1611        for RespawnEvent(entity) in events {
1612            // Hardcore characters cannot respawn
1613            if !hardcore.contains(entity) && clients.contains(entity) {
1614                let respawn_point = waypoints
1615                    .get(entity)
1616                    .map(|wp| wp.get_pos())
1617                    .unwrap_or(spawn_point.0);
1618
1619                healths.get_mut(entity).map(|mut health| health.revive());
1620                combos.get_mut(entity).map(|mut combo| combo.reset());
1621                positions.get_mut(entity).map(|pos| pos.0 = respawn_point);
1622                heads.get_mut(entity).map(|mut heads| heads.reset());
1623                physic_states
1624                    .get_mut(entity)
1625                    .map(|phys_state| phys_state.reset());
1626                force_updates
1627                    .get_mut(entity)
1628                    .map(|force_update| force_update.update());
1629            }
1630        }
1631    }
1632}
1633
1634#[derive(SystemData)]
1635pub struct ExplosionData<'a> {
1636    entities: Entities<'a>,
1637    block_change: Write<'a, BlockChange>,
1638    scheduled_block_change: WriteExpect<'a, ScheduledBlockChange>,
1639    settings: Read<'a, Settings>,
1640    time: Read<'a, Time>,
1641    id_maps: Read<'a, IdMaps>,
1642    spatial_grid: Read<'a, CachedSpatialGrid>,
1643    terrain: ReadExpect<'a, TerrainGrid>,
1644    msm: ReadExpect<'a, MaterialStatManifest>,
1645    event_busses: ReadExplosionEvents<'a>,
1646    outcomes: Read<'a, EventBus<Outcome>>,
1647    groups: ReadStorage<'a, Group>,
1648    auras: ReadStorage<'a, Auras>,
1649    positions: ReadStorage<'a, Pos>,
1650    players: ReadStorage<'a, Player>,
1651    energies: ReadStorage<'a, Energy>,
1652    combos: ReadStorage<'a, comp::Combo>,
1653    inventories: ReadStorage<'a, Inventory>,
1654    alignments: ReadStorage<'a, Alignment>,
1655    entered_auras: ReadStorage<'a, EnteredAuras>,
1656    buffs: ReadStorage<'a, comp::Buffs>,
1657    stats: ReadStorage<'a, comp::Stats>,
1658    healths: ReadStorage<'a, Health>,
1659    bodies: ReadStorage<'a, Body>,
1660    orientations: ReadStorage<'a, comp::Ori>,
1661    character_states: ReadStorage<'a, CharacterState>,
1662    physics_states: ReadStorage<'a, PhysicsState>,
1663    uids: ReadStorage<'a, Uid>,
1664    masses: ReadStorage<'a, comp::Mass>,
1665}
1666
1667impl ServerEvent for ExplosionEvent {
1668    type SystemData<'a> = ExplosionData<'a>;
1669
1670    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
1671        let mut emitters = data.event_busses.get_emitters();
1672        let mut outcome_emitter = data.outcomes.emitter();
1673
1674        // TODO: Faster RNG?
1675        let mut rng = rand::rng();
1676
1677        for ev in events {
1678            let owner_entity = ev.owner.and_then(|uid| data.id_maps.uid_entity(uid));
1679
1680            let explosion_volume = 6.25 * ev.explosion.radius;
1681
1682            emitters.emit(SoundEvent {
1683                sound: Sound::new(SoundKind::Explosion, ev.pos, explosion_volume, data.time.0),
1684            });
1685
1686            let outcome_power = ev.explosion.radius;
1687            outcome_emitter.emit(Outcome::Explosion {
1688                pos: ev.pos,
1689                power: outcome_power,
1690                radius: ev.explosion.radius,
1691                is_attack: ev
1692                    .explosion
1693                    .effects
1694                    .iter()
1695                    .any(|e| matches!(e, RadiusEffect::Attack { .. })),
1696                reagent: ev.explosion.reagent,
1697            });
1698
1699            /// Used to get strength of explosion effects as they falloff over
1700            /// distance
1701            fn cylinder_sphere_strength(
1702                sphere_pos: Vec3<f32>,
1703                radius: f32,
1704                min_falloff: f32,
1705                cyl_pos: Vec3<f32>,
1706                cyl_body: Body,
1707            ) -> f32 {
1708                // 2d check
1709                let horiz_dist = Vec2::<f32>::from(sphere_pos - cyl_pos).distance(Vec2::default())
1710                    - cyl_body.max_radius();
1711                // z check
1712                let half_body_height = cyl_body.height() / 2.0;
1713                let vert_distance =
1714                    (sphere_pos.z - (cyl_pos.z + half_body_height)).abs() - half_body_height;
1715
1716                // Use whichever gives maximum distance as that closer to real value. Sets
1717                // minimum to 0 as negative values would indicate inside entity.
1718                let distance = horiz_dist.max(vert_distance).max(0.0);
1719
1720                if distance > radius {
1721                    // If further than exploion radius, no strength
1722                    0.0
1723                } else {
1724                    // Falloff inversely proportional to radius
1725                    let fall_off = ((distance / radius).min(1.0) - 1.0).abs();
1726                    let min_falloff = min_falloff.clamp(0.0, 1.0);
1727                    min_falloff + fall_off * (1.0 - min_falloff)
1728                }
1729            }
1730
1731            // TODO: Process terrain destruction first so that entities don't get protected
1732            // by terrain that gets destroyed?
1733            'effects: for effect in ev.explosion.effects {
1734                match effect {
1735                    RadiusEffect::TerrainDestruction(power, new_color) => {
1736                        const RAYS: usize = 500;
1737
1738                        // Prevent block colour changes within the radius of a safe zone aura
1739                        if data
1740                            .spatial_grid
1741                            .0
1742                            .in_circle_aabr(ev.pos.xy(), SAFE_ZONE_RADIUS)
1743                            .filter_map(|entity| {
1744                                data.auras
1745                                    .get(entity)
1746                                    .zip(data.positions.get(entity))
1747                                    .and_then(|(entity_auras, pos)| {
1748                                        entity_auras
1749                                            .auras
1750                                            .iter()
1751                                            .find(|(_, aura)| {
1752                                                matches!(aura.aura_kind, aura::AuraKind::Buff {
1753                                                    kind: BuffKind::Invulnerability,
1754                                                    source: BuffSource::World,
1755                                                    ..
1756                                                })
1757                                            })
1758                                            .map(|(_, aura)| (*pos, aura.radius))
1759                                    })
1760                            })
1761                            .any(|(aura_pos, aura_radius)| {
1762                                ev.pos.distance_squared(aura_pos.0) < aura_radius.powi(2)
1763                            })
1764                        {
1765                            continue 'effects;
1766                        }
1767
1768                        // Color terrain
1769                        let mut touched_blocks = Vec::new();
1770                        let color_range = power * 2.7;
1771                        for _ in 0..RAYS {
1772                            let dir = Vec3::new(
1773                                rng.random::<f32>() - 0.5,
1774                                rng.random::<f32>() - 0.5,
1775                                rng.random::<f32>() - 0.5,
1776                            )
1777                            .normalized();
1778
1779                            let _ = data
1780                                .terrain
1781                                .ray(ev.pos, ev.pos + dir * color_range)
1782                                .until(|_| rng.random::<f32>() < 0.05)
1783                                .for_each(|_: &Block, pos| touched_blocks.push(pos))
1784                                .cast();
1785                        }
1786
1787                        for block_pos in touched_blocks {
1788                            if let Ok(block) = data.terrain.get(block_pos) {
1789                                if !matches!(block.kind(), BlockKind::Lava | BlockKind::GlowingRock)
1790                                    && (
1791                                        // Check that owner is not player or explosion_burn_marks by
1792                                        // players
1793                                        // is enabled
1794                                        owner_entity.is_none_or(|e| data.players.get(e).is_none())
1795                                            || data.settings.gameplay.explosion_burn_marks
1796                                    )
1797                                {
1798                                    let diff2 =
1799                                        block_pos.map(|b| b as f32).distance_squared(ev.pos);
1800                                    let fade = (1.0 - diff2 / color_range.powi(2)).max(0.0);
1801                                    if let Some(mut color) = block.get_color() {
1802                                        let r = color[0] as f32
1803                                            + (fade
1804                                                * (color[0] as f32 * 0.5 - color[0] as f32
1805                                                    + new_color[0]));
1806                                        let g = color[1] as f32
1807                                            + (fade
1808                                                * (color[1] as f32 * 0.3 - color[1] as f32
1809                                                    + new_color[1]));
1810                                        let b = color[2] as f32
1811                                            + (fade
1812                                                * (color[2] as f32 * 0.3 - color[2] as f32
1813                                                    + new_color[2]));
1814                                        // Darken blocks, but not too much
1815                                        color[0] = (r as u8).max(30);
1816                                        color[1] = (g as u8).max(30);
1817                                        color[2] = (b as u8).max(30);
1818                                        data.block_change
1819                                            .set(block_pos, Block::new(block.kind(), color));
1820                                    }
1821                                }
1822
1823                                if block.is_bonkable() {
1824                                    emitters.emit(BonkEvent {
1825                                        pos: block_pos.map(|e| e as f32 + 0.5),
1826                                        owner: ev.owner,
1827                                        target: None,
1828                                    });
1829                                }
1830                            }
1831                        }
1832
1833                        // Destroy terrain
1834                        for _ in 0..RAYS {
1835                            let dir = Vec3::new(
1836                                rng.random::<f32>() - 0.5,
1837                                rng.random::<f32>() - 0.5,
1838                                rng.random::<f32>() - 0.15,
1839                            )
1840                            .normalized();
1841
1842                            let mut ray_energy = power;
1843
1844                            let from = ev.pos;
1845                            let to = ev.pos + dir * power;
1846                            let _ = data
1847                                .terrain
1848                                .ray(from, to)
1849                                .while_(|block: &Block| {
1850                                    ray_energy -= block.explode_power().unwrap_or(0.0)
1851                                        + rng.random::<f32>() * 0.1;
1852
1853                                    // Stop if:
1854                                    // 1) Block is liquid
1855                                    // 2) Consumed all energy
1856                                    // 3) Can't explode block (for example we hit stone wall)
1857                                    block.is_liquid()
1858                                        || block.explode_power().is_none()
1859                                        || ray_energy <= 0.0
1860                                })
1861                                .for_each(|block: &Block, pos| {
1862                                    if block.explode_power().is_some() {
1863                                        data.block_change.set(pos, block.into_vacant());
1864                                    }
1865                                })
1866                                .cast();
1867                        }
1868                    },
1869                    RadiusEffect::ReplaceTerrain(radius, terrain_replacement_preset) => {
1870                        const RAY_DENSITY: f32 = 20.0;
1871                        const RAY_LENGTH: f32 = 50.0;
1872
1873                        // Prevent block colour changes within the radius of a safe zone aura
1874                        if data
1875                            .spatial_grid
1876                            .0
1877                            .in_circle_aabr(ev.pos.xy(), SAFE_ZONE_RADIUS)
1878                            .filter_map(|entity| {
1879                                data.auras
1880                                    .get(entity)
1881                                    .zip(data.positions.get(entity))
1882                                    .and_then(|(entity_auras, pos)| {
1883                                        entity_auras
1884                                            .auras
1885                                            .iter()
1886                                            .find(|(_, aura)| {
1887                                                matches!(aura.aura_kind, aura::AuraKind::Buff {
1888                                                    kind: BuffKind::Invulnerability,
1889                                                    source: BuffSource::World,
1890                                                    ..
1891                                                })
1892                                            })
1893                                            .map(|(_, aura)| (*pos, aura.radius))
1894                                    })
1895                            })
1896                            .any(|(aura_pos, aura_radius)| {
1897                                ev.pos.distance_squared(aura_pos.0) < aura_radius.powi(2)
1898                            })
1899                        {
1900                            continue 'effects;
1901                        }
1902
1903                        // Replace terrain
1904                        let mut touched_blocks = Vec::new();
1905                        let height = data
1906                            .terrain
1907                            .ray(ev.pos, ev.pos - RAY_LENGTH * Vec3::unit_z())
1908                            .until(Block::is_solid)
1909                            .cast()
1910                            .0;
1911                        let max_phi = (height / radius).atan();
1912                        for _ in 0..(RAY_DENSITY * radius.powi(2)) as usize {
1913                            let phi = rng.random_range(-PI / 2.0..-max_phi);
1914                            let theta = rng.random_range(0.0..2.0 * PI);
1915                            let ray = Vec3::new(
1916                                RAY_LENGTH * phi.cos() * theta.cos(),
1917                                RAY_LENGTH * phi.cos() * theta.sin(),
1918                                RAY_LENGTH * phi.sin(),
1919                            );
1920
1921                            let _ = data
1922                                .terrain
1923                                .ray(ev.pos, ev.pos + ray)
1924                                .until(Block::is_solid)
1925                                .for_each(|_: &Block, pos| touched_blocks.push(pos))
1926                                .cast();
1927                        }
1928
1929                        for block_pos in touched_blocks {
1930                            if let Ok(block) = data.terrain.get(block_pos) {
1931                                match terrain_replacement_preset {
1932                                    TerrainReplacementPreset::Lava {
1933                                        timeout,
1934                                        timeout_offset,
1935                                        timeout_chance,
1936                                    } => {
1937                                        if !matches!(
1938                                            block.kind(),
1939                                            BlockKind::Air
1940                                                | BlockKind::Water
1941                                                | BlockKind::Lava
1942                                                | BlockKind::GlowingRock
1943                                        ) {
1944                                            data.block_change.set(
1945                                                block_pos,
1946                                                Block::new(BlockKind::Lava, Rgb::new(255, 65, 0)),
1947                                            );
1948
1949                                            if rng.random_bool(timeout_chance as f64) {
1950                                                let current_time: f64 = data.time.0;
1951                                                let replace_time = current_time
1952                                                    + (timeout
1953                                                        + rng.random_range(0.0..timeout_offset))
1954                                                        as f64;
1955                                                data.scheduled_block_change.set(
1956                                                    block_pos,
1957                                                    Block::new(
1958                                                        BlockKind::Rock,
1959                                                        Rgb::new(12, 10, 25),
1960                                                    ),
1961                                                    replace_time,
1962                                                );
1963                                            }
1964                                        }
1965                                    },
1966                                }
1967                            }
1968                        }
1969                    },
1970                    RadiusEffect::Attack { attack, dodgeable } => {
1971                        for (
1972                            entity_b,
1973                            pos_b,
1974                            health_b,
1975                            (
1976                                body_b_maybe,
1977                                ori_b_maybe,
1978                                char_state_b_maybe,
1979                                physics_state_b_maybe,
1980                                uid_b,
1981                            ),
1982                        ) in (
1983                            &data.entities,
1984                            &data.positions,
1985                            &data.healths,
1986                            (
1987                                data.bodies.maybe(),
1988                                data.orientations.maybe(),
1989                                data.character_states.maybe(),
1990                                data.physics_states.maybe(),
1991                                &data.uids,
1992                            ),
1993                        )
1994                            .join()
1995                            .filter(|(_, _, h, _)| !h.is_dead)
1996                        {
1997                            let pos_b = Pos(pos_b.0
1998                                + Vec3::unit_z() * body_b_maybe.map_or(0.5, |b| b.height() / 2.0));
1999                            let dist_sqrd = ev.pos.distance_squared(pos_b.0);
2000
2001                            // Check if it is a hit
2002                            let strength = if let Some(body) = body_b_maybe {
2003                                cylinder_sphere_strength(
2004                                    ev.pos,
2005                                    ev.explosion.radius,
2006                                    ev.explosion.min_falloff,
2007                                    pos_b.0,
2008                                    *body,
2009                                )
2010                            } else {
2011                                1.0 - dist_sqrd / ev.explosion.radius.powi(2)
2012                            };
2013
2014                            // Cast a ray from the explosion to the entity to check visibility
2015                            if strength > 0.0
2016                                && (data
2017                                    .terrain
2018                                    .ray(ev.pos, pos_b.0)
2019                                    .until(Block::is_opaque)
2020                                    .cast()
2021                                    .0
2022                                    + 0.1)
2023                                    .powi(2)
2024                                    >= dist_sqrd
2025                            {
2026                                // See if entities are in the same group
2027                                let same_group = owner_entity
2028                                    .and_then(|e| data.groups.get(e))
2029                                    .map(|group_a| Some(group_a) == data.groups.get(entity_b))
2030                                    .unwrap_or(Some(entity_b) == owner_entity);
2031
2032                                let target_group = if same_group {
2033                                    GroupTarget::InGroup
2034                                } else {
2035                                    GroupTarget::OutOfGroup
2036                                };
2037
2038                                let dir = Dir::new(
2039                                    (pos_b.0 - ev.pos)
2040                                        .try_normalized()
2041                                        .unwrap_or_else(Vec3::unit_z),
2042                                );
2043
2044                                let attacker_info =
2045                                    owner_entity.zip(ev.owner).map(|(entity, uid)| {
2046                                        combat::AttackerInfo {
2047                                            entity,
2048                                            uid,
2049                                            group: data.groups.get(entity),
2050                                            energy: data.energies.get(entity),
2051                                            combo: data.combos.get(entity),
2052                                            inventory: data.inventories.get(entity),
2053                                            stats: data.stats.get(entity),
2054                                            mass: data.masses.get(entity),
2055                                            pos: data.positions.get(entity).map(|p| p.0),
2056                                        }
2057                                    });
2058
2059                                let target_info = combat::TargetInfo {
2060                                    entity: entity_b,
2061                                    uid: *uid_b,
2062                                    inventory: data.inventories.get(entity_b),
2063                                    stats: data.stats.get(entity_b),
2064                                    health: Some(health_b),
2065                                    pos: pos_b.0,
2066                                    ori: ori_b_maybe,
2067                                    char_state: char_state_b_maybe,
2068                                    energy: data.energies.get(entity_b),
2069                                    buffs: data.buffs.get(entity_b),
2070                                    mass: data.masses.get(entity_b),
2071                                    player: data.players.get(entity_b),
2072                                };
2073
2074                                // Check if entity is dodging
2075                                let target_dodging = match dodgeable {
2076                                    Dodgeable::Roll => char_state_b_maybe
2077                                        .and_then(|cs| cs.roll_attack_immunities())
2078                                        .is_some_and(|i| i.melee),
2079                                    Dodgeable::Jump => physics_state_b_maybe
2080                                        .is_some_and(|ps| ps.on_ground.is_none()),
2081                                    Dodgeable::No => false,
2082                                };
2083                                let allow_friendly_fire =
2084                                    owner_entity.is_some_and(|owner_entity| {
2085                                        combat::allow_friendly_fire(
2086                                            &data.entered_auras,
2087                                            owner_entity,
2088                                            entity_b,
2089                                        )
2090                                    });
2091                                // PvP check
2092                                let permit_pvp = combat::permit_pvp(
2093                                    &data.alignments,
2094                                    &data.players,
2095                                    &data.entered_auras,
2096                                    &data.id_maps,
2097                                    owner_entity,
2098                                    entity_b,
2099                                );
2100                                let attack_options = combat::AttackOptions {
2101                                    target_dodging,
2102                                    permit_pvp,
2103                                    allow_friendly_fire,
2104                                    target_group,
2105                                    precision_mult: None,
2106                                };
2107
2108                                attack.apply_attack(
2109                                    attacker_info,
2110                                    &target_info,
2111                                    dir,
2112                                    attack_options,
2113                                    strength,
2114                                    combat::AttackSource::Explosion,
2115                                    *data.time,
2116                                    &mut emitters,
2117                                    |o| outcome_emitter.emit(o),
2118                                    &mut rng,
2119                                    0,
2120                                );
2121                            }
2122                        }
2123                    },
2124                    RadiusEffect::Entity(mut effect) => {
2125                        for (entity_b, pos_b, body_b_maybe) in
2126                            (&data.entities, &data.positions, data.bodies.maybe()).join()
2127                        {
2128                            let strength = if let Some(body) = body_b_maybe {
2129                                cylinder_sphere_strength(
2130                                    ev.pos,
2131                                    ev.explosion.radius,
2132                                    ev.explosion.min_falloff,
2133                                    pos_b.0,
2134                                    *body,
2135                                )
2136                            } else {
2137                                let distance_squared = ev.pos.distance_squared(pos_b.0);
2138                                1.0 - distance_squared / ev.explosion.radius.powi(2)
2139                            };
2140
2141                            // Player check only accounts for PvP/PvE flag (unless in a friendly
2142                            // fire aura), but bombs are intented to do
2143                            // friendly fire.
2144                            //
2145                            // What exactly is friendly fire is subject to discussion.
2146                            // As we probably want to minimize possibility of being dick
2147                            // even to your group members, the only exception is when
2148                            // you want to harm yourself.
2149                            //
2150                            // This can be changed later.
2151                            let permit_pvp = || {
2152                                combat::permit_pvp(
2153                                    &data.alignments,
2154                                    &data.players,
2155                                    &data.entered_auras,
2156                                    &data.id_maps,
2157                                    owner_entity,
2158                                    entity_b,
2159                                ) || owner_entity.is_none_or(|entity_a| entity_a == entity_b)
2160                            };
2161                            if strength > 0.0 {
2162                                let is_alive =
2163                                    data.healths.get(entity_b).is_none_or(|h| !h.is_dead);
2164
2165                                if is_alive {
2166                                    effect.modify_strength(strength);
2167                                    if !effect.is_harm() || permit_pvp() {
2168                                        emit_effect_events(
2169                                            &mut emitters,
2170                                            *data.time,
2171                                            entity_b,
2172                                            effect.clone(),
2173                                            ev.owner.map(|owner| {
2174                                                (
2175                                                    owner,
2176                                                    data.id_maps
2177                                                        .uid_entity(owner)
2178                                                        .and_then(|e| data.groups.get(e))
2179                                                        .copied(),
2180                                                )
2181                                            }),
2182                                            data.inventories.get(entity_b),
2183                                            &data.msm,
2184                                            data.character_states.get(entity_b),
2185                                            data.stats.get(entity_b),
2186                                            data.masses.get(entity_b),
2187                                            owner_entity.and_then(|e| data.masses.get(e)),
2188                                            data.bodies.get(entity_b),
2189                                            data.positions.get(entity_b),
2190                                        );
2191                                    }
2192                                }
2193                            }
2194                        }
2195                    },
2196                }
2197            }
2198        }
2199    }
2200}
2201
2202pub fn emit_effect_events(
2203    emitters: &mut (
2204             impl EmitExt<HealthChangeEvent>
2205             + EmitExt<PoiseChangeEvent>
2206             + EmitExt<BuffEvent>
2207             + EmitExt<ChangeBodyEvent>
2208             + EmitExt<Outcome>
2209             + EmitExt<ChangeStanceEvent>
2210         ),
2211    time: Time,
2212    entity: EcsEntity,
2213    effect: common::effect::Effect,
2214    source: Option<(Uid, Option<Group>)>,
2215    inventory: Option<&Inventory>,
2216    msm: &MaterialStatManifest,
2217    char_state: Option<&CharacterState>,
2218    stats: Option<&Stats>,
2219    tgt_mass: Option<&comp::Mass>,
2220    source_mass: Option<&comp::Mass>,
2221    tgt_body: Option<&Body>,
2222    tgt_pos: Option<&Pos>,
2223) {
2224    let damage_contributor = source.map(|(uid, group)| DamageContributor::new(uid, group));
2225    match effect {
2226        common::effect::Effect::Health(change) => {
2227            emitters.emit(HealthChangeEvent { entity, change })
2228        },
2229        common::effect::Effect::Poise(amount) => {
2230            let amount = Poise::apply_poise_reduction(amount, inventory, msm, char_state, stats);
2231            emitters.emit(PoiseChangeEvent {
2232                entity,
2233                change: comp::PoiseChange {
2234                    amount,
2235                    impulse: Vec3::zero(),
2236                    by: damage_contributor,
2237                    cause: None,
2238                    time,
2239                },
2240            })
2241        },
2242        common::effect::Effect::Damage(damage) => {
2243            let change = damage.calculate_health_change(
2244                combat::Damage::compute_damage_reduction(Some(damage), inventory, stats, msm),
2245                0.0,
2246                damage_contributor,
2247                None,
2248                0.0,
2249                1.0,
2250                time,
2251                rand::random(),
2252                DamageSource::Other,
2253            );
2254            emitters.emit(HealthChangeEvent { entity, change })
2255        },
2256        common::effect::Effect::Buff(buff) => {
2257            let dest_info = buff::DestInfo {
2258                stats,
2259                mass: tgt_mass,
2260            };
2261            emitters.emit(BuffEvent {
2262                entity,
2263                buff_change: comp::BuffChange::Add(comp::Buff::new(
2264                    buff.kind,
2265                    buff.data,
2266                    buff.cat_ids,
2267                    comp::BuffSource::Item,
2268                    time,
2269                    dest_info,
2270                    source_mass,
2271                )),
2272            });
2273        },
2274        common::effect::Effect::Permanent(permanent_effect) => match permanent_effect {
2275            common::effect::PermanentEffect::CycleBodyType => {
2276                if let Some(body) = tgt_body
2277                    && let Some(new_body) = match body {
2278                        Body::Humanoid(body) => Some(Body::Humanoid(comp::humanoid::Body {
2279                            body_type: match body.body_type {
2280                                comp::humanoid::BodyType::Female => comp::humanoid::BodyType::Male,
2281                                comp::humanoid::BodyType::Male => comp::humanoid::BodyType::Female,
2282                            },
2283                            ..*body
2284                        })),
2285                        // Only allow humanoids for now.
2286                        _ => None,
2287                    }
2288                {
2289                    // TODO: Change only the body from the character list?
2290                    emitters.emit(ChangeBodyEvent {
2291                        entity,
2292                        new_body,
2293                        permanent_change: Some(PermanentChange {
2294                            expected_old_body: *body,
2295                        }),
2296                    });
2297                    if let Some(pos) = tgt_pos {
2298                        emitters.emit(Outcome::Transformation { pos: pos.0 });
2299                    }
2300                }
2301            },
2302        },
2303        common::effect::Effect::Stance(stance) => {
2304            emitters.emit(ChangeStanceEvent { entity, stance });
2305        },
2306    }
2307}
2308
2309impl ServerEvent for BonkEvent {
2310    type SystemData<'a> = (
2311        Write<'a, BlockChange>,
2312        ReadExpect<'a, TerrainGrid>,
2313        ReadExpect<'a, ProgramTime>,
2314        Read<'a, EventBus<CreateObjectEvent>>,
2315        Read<'a, EventBus<ShootEvent>>,
2316    );
2317
2318    fn handle(
2319        events: impl ExactSizeIterator<Item = Self>,
2320        (mut block_change, terrain, program_time, create_object_events, shoot_events): Self::SystemData<'_>,
2321    ) {
2322        let mut create_object_emitter = create_object_events.emitter();
2323        let mut shoot_emitter = shoot_events.emitter();
2324        for ev in events {
2325            if let Some(_target) = ev.target {
2326                // TODO: bonk entities but do no damage?
2327            } else {
2328                use common::terrain::SpriteKind;
2329                let pos = ev.pos.map(|e| e.floor() as i32);
2330                if let Some(block) = terrain.get(pos).ok().copied().filter(|b| b.is_bonkable())
2331                    && block_change
2332                        .try_set(pos, block.with_sprite(SpriteKind::Empty))
2333                        .is_some()
2334                {
2335                    let sprite_cfg = terrain.sprite_cfg_at(pos);
2336                    if let Some(items) = comp::Item::try_reclaim_from_block(block, sprite_cfg) {
2337                        let msm = &MaterialStatManifest::load().read();
2338                        let ability_map = &AbilityMap::load().read();
2339                        for item in flatten_counted_items(&items, ability_map, msm) {
2340                            let pos = Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0));
2341                            let vel = comp::Vel::default();
2342                            // TODO: Use the `ItemDrop` body for this.
2343                            let body = match block.get_sprite() {
2344                                // Create different containers depending on the original
2345                                // sprite
2346                                Some(SpriteKind::Apple) => comp::object::Body::Apple,
2347                                Some(SpriteKind::Beehive) => comp::object::Body::Hive,
2348                                Some(SpriteKind::Coconut) => comp::object::Body::Coconut,
2349                                Some(SpriteKind::Bomb) => comp::object::Body::Bomb,
2350                                _ => comp::object::Body::Pebble,
2351                            };
2352
2353                            if matches!(block.get_sprite(), Some(SpriteKind::Bomb)) {
2354                                shoot_emitter.emit(ShootEvent {
2355                                    entity: None,
2356                                    source_vel: None,
2357                                    pos,
2358                                    dir: Dir::from_unnormalized(vel.0).unwrap_or_default(),
2359                                    body: Body::Object(body),
2360                                    light: None,
2361                                    projectile: ProjectileConstructor {
2362                                        kind: ProjectileConstructorKind::Explosive {
2363                                            radius: 12.0,
2364                                            min_falloff: 0.75,
2365                                            reagent: None,
2366                                            terrain: Some((4.0, ColorPreset::Black)),
2367                                        },
2368                                        attack: Some(ProjectileAttack {
2369                                            damage: 40.0,
2370                                            poise: Some(100.0),
2371                                            knockback: None,
2372                                            energy: None,
2373                                            buff: None,
2374                                            friendly_fire: true,
2375                                            blockable: true,
2376                                            attack_effect: None,
2377                                            damage_effect: None,
2378                                            without_combo: false,
2379                                        }),
2380                                        scaled: None,
2381                                        homing_rate: None,
2382                                        split: None,
2383                                        lifetime_override: None,
2384                                        limit_per_ability: false,
2385                                        override_collider: None,
2386                                    }
2387                                    .create_projectile(None, 1.0, None),
2388                                    speed: vel.0.magnitude(),
2389                                    object: None,
2390                                    marker: None,
2391                                });
2392                            } else {
2393                                create_object_emitter.emit(CreateObjectEvent {
2394                                    pos,
2395                                    vel,
2396                                    body,
2397                                    object: None,
2398                                    item: Some(comp::PickupItem::new(item, *program_time, false)),
2399                                    light_emitter: None,
2400                                    stats: None,
2401                                });
2402                            }
2403                        }
2404                    }
2405                }
2406            }
2407        }
2408    }
2409}
2410
2411impl ServerEvent for AuraEvent {
2412    type SystemData<'a> = (WriteStorage<'a, Auras>, WriteStorage<'a, EnteredAuras>);
2413
2414    fn handle(
2415        events: impl ExactSizeIterator<Item = Self>,
2416        (mut auras, mut entered_auras): Self::SystemData<'_>,
2417    ) {
2418        for ev in events {
2419            use aura::AuraChange;
2420            match ev.aura_change {
2421                AuraChange::Add(new_aura) => {
2422                    if let Some(mut auras) = auras.get_mut(ev.entity) {
2423                        auras.insert(new_aura);
2424                    }
2425                },
2426                AuraChange::RemoveByKey(keys) => {
2427                    if let Some(mut auras) = auras.get_mut(ev.entity) {
2428                        for key in keys {
2429                            auras.remove(key);
2430                        }
2431                    }
2432                },
2433                AuraChange::EnterAura(uid, key, variant) => {
2434                    if let Some(mut entered_auras) = entered_auras.get_mut(ev.entity) {
2435                        entered_auras
2436                            .auras
2437                            .entry(variant)
2438                            .and_modify(|entered_auras| {
2439                                entered_auras.insert((uid, key));
2440                            })
2441                            .or_insert_with(|| <_ as Into<_>>::into([(uid, key)]));
2442                    }
2443                },
2444                AuraChange::ExitAura(uid, key, variant) => {
2445                    if let Some(mut entered_auras) = entered_auras.get_mut(ev.entity)
2446                        && let Some(entered_auras_variant) = entered_auras.auras.get_mut(&variant)
2447                    {
2448                        entered_auras_variant.remove(&(uid, key));
2449
2450                        if entered_auras_variant.is_empty() {
2451                            entered_auras.auras.remove(&variant);
2452                        }
2453                    }
2454                },
2455            }
2456        }
2457    }
2458}
2459
2460impl ServerEvent for BuffEvent {
2461    type SystemData<'a> = (
2462        Read<'a, Time>,
2463        WriteStorage<'a, comp::Buffs>,
2464        ReadStorage<'a, Body>,
2465        ReadStorage<'a, Health>,
2466        ReadStorage<'a, Stats>,
2467        ReadStorage<'a, comp::Mass>,
2468    );
2469
2470    fn handle(
2471        events: impl ExactSizeIterator<Item = Self>,
2472        (time, mut buffs, bodies, healths, stats, masses): Self::SystemData<'_>,
2473    ) {
2474        for ev in events {
2475            if let Some(mut buffs) = buffs.get_mut(ev.entity) {
2476                use buff::BuffChange;
2477                match ev.buff_change {
2478                    BuffChange::Add(mut new_buff) => {
2479                        let immunity_by_buff = buffs
2480                            .buffs
2481                            .values_mut()
2482                            .flat_map(|b| {
2483                                b.kind.effects(
2484                                    &b.data,
2485                                    if let BuffSource::Character { by, .. } = b.source {
2486                                        Some(by)
2487                                    } else {
2488                                        None
2489                                    },
2490                                )
2491                            })
2492                            .find(|b| match b {
2493                                BuffEffect::BuffImmunity(kind) => new_buff.kind == *kind,
2494                                _ => false,
2495                            });
2496
2497                        if !bodies
2498                            .get(ev.entity)
2499                            .is_some_and(|body| body.immune_to(new_buff.kind))
2500                            && immunity_by_buff.is_none()
2501                            && healths.get(ev.entity).is_none_or(|h| !h.is_dead)
2502                        {
2503                            if let Some(strength) =
2504                                new_buff.kind.resilience_ccr_strength(new_buff.data)
2505                            {
2506                                let resilience_buff = buff::Buff::new(
2507                                    BuffKind::Resilience,
2508                                    buff::BuffData::new(
2509                                        strength,
2510                                        Some(
2511                                            new_buff
2512                                                .data
2513                                                .duration
2514                                                .map_or(Secs(30.0), |dur| dur * 5.0),
2515                                        ),
2516                                    ),
2517                                    Vec::new(),
2518                                    BuffSource::Buff,
2519                                    *time,
2520                                    buff::DestInfo {
2521                                        stats: stats.get(ev.entity),
2522                                        mass: masses.get(ev.entity),
2523                                    },
2524                                    // There is no source entity
2525                                    None,
2526                                );
2527                                buffs.insert(resilience_buff, *time);
2528                            }
2529
2530                            if bodies
2531                                .get(ev.entity)
2532                                .is_some_and(|body| body.negates_buff(new_buff.kind))
2533                            {
2534                                new_buff.effects.clear();
2535                            }
2536
2537                            buffs.insert(new_buff, *time);
2538                        }
2539                    },
2540                    BuffChange::RemoveByKey(keys) => {
2541                        for key in keys {
2542                            buffs.remove(key);
2543                        }
2544                    },
2545                    BuffChange::RemoveByKind(kind) => {
2546                        buffs.remove_kind(kind);
2547                    },
2548                    BuffChange::RemoveFromController(kind) => {
2549                        if kind.is_buff() {
2550                            buffs.remove_kind(kind);
2551                        }
2552                    },
2553                    BuffChange::RemoveByCategory {
2554                        all_required,
2555                        any_required,
2556                        none_required,
2557                    } => {
2558                        let mut keys_to_remove = Vec::new();
2559                        for (key, buff) in buffs.buffs.iter() {
2560                            let mut required_met = true;
2561                            for required in &all_required {
2562                                if !buff.cat_ids.iter().any(|cat| cat == required) {
2563                                    required_met = false;
2564                                    break;
2565                                }
2566                            }
2567                            let mut any_met = any_required.is_empty();
2568                            for any in &any_required {
2569                                if buff.cat_ids.iter().any(|cat| cat == any) {
2570                                    any_met = true;
2571                                    break;
2572                                }
2573                            }
2574                            let mut none_met = true;
2575                            for none in &none_required {
2576                                if buff.cat_ids.iter().any(|cat| cat == none) {
2577                                    none_met = false;
2578                                    break;
2579                                }
2580                            }
2581                            if required_met && any_met && none_met {
2582                                keys_to_remove.push(key);
2583                            }
2584                        }
2585                        for key in keys_to_remove {
2586                            buffs.remove(key);
2587                        }
2588                    },
2589                    BuffChange::Refresh(kind) => {
2590                        buffs
2591                            .buffs
2592                            .values_mut()
2593                            .filter(|b| b.kind == kind)
2594                            .for_each(|buff| {
2595                                // Resets buff so that its remaining duration is equal to its
2596                                // original duration
2597                                buff.start_time = *time;
2598                                buff.end_time = buff.data.duration.map(|dur| Time(time.0 + dur.0));
2599                            })
2600                    },
2601                }
2602            }
2603        }
2604    }
2605}
2606
2607impl ServerEvent for EnergyChangeEvent {
2608    type SystemData<'a> = WriteStorage<'a, Energy>;
2609
2610    fn handle(events: impl ExactSizeIterator<Item = Self>, mut energies: Self::SystemData<'_>) {
2611        for ev in events {
2612            if let Some(mut energy) = energies.get_mut(ev.entity) {
2613                energy.change_by(ev.change);
2614                if ev.reset_rate {
2615                    energy.reset_regen_rate();
2616                }
2617            }
2618        }
2619    }
2620}
2621
2622impl ServerEvent for ComboChangeEvent {
2623    type SystemData<'a> = (
2624        Read<'a, Time>,
2625        Read<'a, EventBus<Outcome>>,
2626        WriteStorage<'a, comp::Combo>,
2627        ReadStorage<'a, Uid>,
2628    );
2629
2630    fn handle(
2631        events: impl ExactSizeIterator<Item = Self>,
2632        (time, outcomes, mut combos, uids): Self::SystemData<'_>,
2633    ) {
2634        let mut outcome_emitter = outcomes.emitter();
2635        for ev in events {
2636            if let Some(mut combo) = combos.get_mut(ev.entity) {
2637                combo.change_by(ev.change, time.0);
2638                if let Some(uid) = uids.get(ev.entity) {
2639                    outcome_emitter.emit(Outcome::ComboChange {
2640                        uid: *uid,
2641                        combo: combo.counter(),
2642                    });
2643                }
2644            }
2645        }
2646    }
2647}
2648
2649impl ServerEvent for ParryHookEvent {
2650    type SystemData<'a> = (
2651        Read<'a, Time>,
2652        Read<'a, EventBus<EnergyChangeEvent>>,
2653        Read<'a, EventBus<PoiseChangeEvent>>,
2654        Read<'a, EventBus<BuffEvent>>,
2655        WriteStorage<'a, CharacterState>,
2656        ReadStorage<'a, Uid>,
2657        ReadStorage<'a, Stats>,
2658        ReadStorage<'a, comp::Mass>,
2659        ReadStorage<'a, Inventory>,
2660    );
2661
2662    fn handle(
2663        events: impl ExactSizeIterator<Item = Self>,
2664        (
2665            time,
2666            energy_change_events,
2667            poise_change_events,
2668            buff_events,
2669            mut character_states,
2670            uids,
2671            stats,
2672            masses,
2673            inventories,
2674        ): Self::SystemData<'_>,
2675    ) {
2676        let mut energy_change_emitter = energy_change_events.emitter();
2677        let mut poise_change_emitter = poise_change_events.emitter();
2678        let mut buff_emitter = buff_events.emitter();
2679        for ev in events {
2680            let mut defender_tool = None;
2681
2682            if let Some(mut char_state) = character_states.get_mut(ev.defender) {
2683                defender_tool = char_state.ability_info().and_then(|ai| ai.tool);
2684                let return_to_wield = match &mut *char_state {
2685                    CharacterState::RiposteMelee(c) => {
2686                        c.stage_section = StageSection::Action;
2687                        c.timer = Duration::default();
2688                        c.whiffed = false;
2689                        false
2690                    },
2691                    CharacterState::BasicBlock(c) => {
2692                        // Refund half the energy of entering the block for a successful parry
2693                        energy_change_emitter.emit(EnergyChangeEvent {
2694                            entity: ev.defender,
2695                            change: c.static_data.energy_regen,
2696                            reset_rate: false,
2697                        });
2698                        c.is_parry = true;
2699                        false
2700                    },
2701                    _ => false,
2702                };
2703                if return_to_wield {
2704                    *char_state = CharacterState::Wielding(common::states::wielding::Data {
2705                        is_sneaking: false,
2706                    });
2707                }
2708            };
2709
2710            if let Some(attacker) = ev.attacker
2711                && matches!(ev.source, AttackSource::Melee)
2712            {
2713                // When attacker is parried, the debuff lasts 2 seconds, the attacker takes
2714                // poise damage, get precision vulnerability and get slower recovery speed
2715                let data = buff::BuffData::new(1.0, Some(Secs(2.0)));
2716                let source = if let Some(uid) = uids.get(ev.defender) {
2717                    BuffSource::Character {
2718                        by: *uid,
2719                        tool_kind: defender_tool,
2720                    }
2721                } else {
2722                    BuffSource::World
2723                };
2724                let dest_info = buff::DestInfo {
2725                    stats: stats.get(attacker),
2726                    mass: masses.get(attacker),
2727                };
2728                let buff = buff::Buff::new(
2729                    BuffKind::Parried,
2730                    data,
2731                    vec![buff::BuffCategory::Physical],
2732                    source,
2733                    *time,
2734                    dest_info,
2735                    masses.get(ev.defender),
2736                );
2737                buff_emitter.emit(BuffEvent {
2738                    entity: attacker,
2739                    buff_change: buff::BuffChange::Add(buff),
2740                });
2741
2742                let attacker_poise_change = Poise::apply_poise_reduction(
2743                    ev.poise_multiplier.clamp(1.0, 2.0) * BASE_PARRIED_POISE_PUNISHMENT,
2744                    inventories.get(attacker),
2745                    &MaterialStatManifest::load().read(),
2746                    character_states.get(attacker),
2747                    stats.get(attacker),
2748                );
2749
2750                poise_change_emitter.emit(PoiseChangeEvent {
2751                    entity: attacker,
2752                    change: PoiseChange {
2753                        amount: -attacker_poise_change,
2754                        impulse: Vec3::zero(),
2755                        by: uids
2756                            .get(ev.defender)
2757                            .map(|d| DamageContributor::new(*d, None)),
2758                        cause: Some(DamageSource::Attack(ev.source)),
2759                        time: *time,
2760                    },
2761                });
2762            }
2763        }
2764    }
2765}
2766
2767impl ServerEvent for TeleportToEvent {
2768    type SystemData<'a> = (
2769        Read<'a, IdMaps>,
2770        WriteStorage<'a, Pos>,
2771        WriteStorage<'a, comp::ForceUpdate>,
2772    );
2773
2774    fn handle(
2775        events: impl ExactSizeIterator<Item = Self>,
2776        (id_maps, mut positions, mut force_updates): Self::SystemData<'_>,
2777    ) {
2778        for ev in events {
2779            let target_pos = id_maps
2780                .uid_entity(ev.target)
2781                .and_then(|e| positions.get(e))
2782                .copied();
2783
2784            if let (Some(pos), Some(target_pos)) = (positions.get_mut(ev.entity), target_pos)
2785                && ev
2786                    .max_range
2787                    .is_none_or(|r| pos.0.distance_squared(target_pos.0) < r.powi(2))
2788            {
2789                *pos = target_pos;
2790                force_updates
2791                    .get_mut(ev.entity)
2792                    .map(|force_update| force_update.update());
2793            }
2794        }
2795    }
2796}
2797
2798#[derive(SystemData)]
2799pub struct EntityAttackedHookData<'a> {
2800    entities: Entities<'a>,
2801    trades: Write<'a, Trades>,
2802    id_maps: Read<'a, IdMaps>,
2803    time: Read<'a, Time>,
2804    event_busses: ReadEntityAttackedHookEvents<'a>,
2805    outcomes: Read<'a, EventBus<Outcome>>,
2806    character_states: WriteStorage<'a, CharacterState>,
2807    poises: WriteStorage<'a, Poise>,
2808    agents: WriteStorage<'a, Agent>,
2809    positions: ReadStorage<'a, Pos>,
2810    uids: ReadStorage<'a, Uid>,
2811    clients: ReadStorage<'a, Client>,
2812    stats: ReadStorage<'a, Stats>,
2813    healths: ReadStorage<'a, Health>,
2814    inventories: ReadStorage<'a, Inventory>,
2815    buffs: ReadStorage<'a, comp::Buffs>,
2816    players: ReadStorage<'a, Player>,
2817    msm: ReadExpect<'a, MaterialStatManifest>,
2818    masses: ReadStorage<'a, comp::Mass>,
2819    groups: ReadStorage<'a, Group>,
2820    orientations: ReadStorage<'a, comp::Ori>,
2821    combos: ReadStorage<'a, comp::Combo>,
2822    energies: ReadStorage<'a, comp::Energy>,
2823}
2824
2825impl ServerEvent for EntityAttackedHookEvent {
2826    type SystemData<'a> = EntityAttackedHookData<'a>;
2827
2828    /// Intended to handle things that should happen for any successful attack,
2829    /// regardless of the damages and effects specific to that attack
2830    fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
2831        let mut emitters = data.event_busses.get_emitters();
2832        let mut outcomes = data.outcomes.emitter();
2833        let mut rng = rand::rng();
2834
2835        for ev in events {
2836            if let Some(attacker) = ev.attacker {
2837                emitters.emit(BuffEvent {
2838                    entity: attacker,
2839                    buff_change: buff::BuffChange::RemoveByCategory {
2840                        all_required: vec![buff::BuffCategory::RemoveOnAttack],
2841                        any_required: vec![],
2842                        none_required: vec![],
2843                    },
2844                });
2845            }
2846
2847            if let Some((mut char_state, mut poise, pos)) = (
2848                &mut data.character_states,
2849                &mut data.poises,
2850                &data.positions,
2851            )
2852                .lend_join()
2853                .get(ev.entity, &data.entities)
2854            {
2855                // Interrupt sprite interaction and item use if any attack is applied to entity
2856                if matches!(
2857                    *char_state,
2858                    CharacterState::Interact(_) | CharacterState::UseItem(_)
2859                ) {
2860                    let poise_state = comp::poise::PoiseState::Interrupted;
2861                    let was_wielded = char_state.is_wield();
2862                    if let (Some((stunned_state, stunned_duration)), impulse_strength) =
2863                        poise_state.poise_effect(was_wielded)
2864                    {
2865                        // Reset poise if there is some stunned state to apply
2866                        poise.reset(*data.time, stunned_duration);
2867                        if !comp::is_downed(data.healths.get(ev.entity), Some(&char_state)) {
2868                            *char_state = stunned_state;
2869                        }
2870                        outcomes.emit(Outcome::PoiseChange {
2871                            pos: pos.0,
2872                            state: poise_state,
2873                        });
2874                        if let Some(impulse_strength) = impulse_strength {
2875                            emitters.emit(KnockbackEvent {
2876                                entity: ev.entity,
2877                                impulse: impulse_strength * *poise.knockback(),
2878                            });
2879                        }
2880                    }
2881                }
2882            }
2883
2884            // Remove potion/saturation buff if attacked
2885            emitters.emit(BuffEvent {
2886                entity: ev.entity,
2887                buff_change: buff::BuffChange::RemoveByKind(BuffKind::Potion),
2888            });
2889            emitters.emit(BuffEvent {
2890                entity: ev.entity,
2891                buff_change: buff::BuffChange::RemoveByKind(BuffKind::Saturation),
2892            });
2893
2894            // If entity was in an active trade, cancel it
2895            if let Some(uid) = data.uids.get(ev.entity)
2896                && let Some(trade) = data.trades.entity_trades.get(uid).copied()
2897            {
2898                data.trades
2899                    .decline_trade(trade, *uid)
2900                    .and_then(|uid| data.id_maps.uid_entity(uid))
2901                    .map(|entity_b| {
2902                        // Notify both parties that the trade ended
2903                        let mut notify_trade_party = |entity| {
2904                            // TODO: Can probably improve UX here for the user that sent the
2905                            // trade invite, since right now it
2906                            // may seems like their request was
2907                            // purposefully declined, rather than e.g. being interrupted.
2908                            if let Some(client) = data.clients.get(entity) {
2909                                client.send_fallible(ServerGeneral::FinishedTrade(
2910                                    TradeResult::Declined,
2911                                ));
2912                            }
2913                            if let Some(agent) = data.agents.get_mut(entity) {
2914                                agent
2915                                    .inbox
2916                                    .push_back(AgentEvent::FinishedTrade(TradeResult::Declined));
2917                            }
2918                        };
2919                        notify_trade_party(ev.entity);
2920                        notify_trade_party(entity_b);
2921                    });
2922            }
2923
2924            if let Some(stats) = data.stats.get(ev.entity) {
2925                for effect in &stats.effects_on_damaged {
2926                    let (effect_target, other_entity) = match effect.target {
2927                        StatEffectTarget::Target => (ev.entity, ev.attacker),
2928                        StatEffectTarget::Attacker => {
2929                            if let Some(attacker) = ev.attacker {
2930                                (attacker, Some(ev.entity))
2931                            } else {
2932                                continue;
2933                            }
2934                        },
2935                    };
2936
2937                    let dir = match effect.target {
2938                        StatEffectTarget::Target => ev.attack_dir,
2939                        StatEffectTarget::Attacker => -ev.attack_dir,
2940                    };
2941
2942                    let dmg_contrib = data.uids.get(ev.entity).map(|uid| {
2943                        DamageContributor::new(*uid, data.groups.get(ev.entity).copied())
2944                    });
2945
2946                    let requirements_met = effect.requirements().all(|req| {
2947                        req.requirement_met(
2948                            (
2949                                data.healths.get(effect_target),
2950                                data.buffs.get(effect_target),
2951                                data.character_states.get(effect_target),
2952                                data.orientations.get(effect_target),
2953                            ),
2954                            (
2955                                Some(ev.entity),
2956                                data.energies.get(ev.entity),
2957                                data.combos.get(ev.entity),
2958                            ),
2959                            ev.attacker.and_then(|e| data.uids.get(e)).copied(),
2960                            ev.damage_dealt,
2961                            &mut emitters,
2962                            dir,
2963                            Some(ev.attack_source),
2964                            None,
2965                        )
2966                    });
2967
2968                    if requirements_met {
2969                        let mut strength_modifier = 1.0;
2970                        for modification in effect.modifications() {
2971                            modification.apply_mod(
2972                                data.positions.get(effect_target).map(|x| x.0),
2973                                data.positions.get(ev.entity).map(|x| x.0),
2974                                &mut strength_modifier,
2975                            );
2976                        }
2977                        let strength_modifier = strength_modifier;
2978
2979                        match &effect.effect {
2980                            CombatEffect::Knockback(kb) => {
2981                                let char_state = data.character_states.get(effect_target);
2982                                let impulse = kb.calculate_impulse(
2983                                    dir,
2984                                    char_state,
2985                                    ev.attacker.and_then(|ae| data.stats.get(ae)),
2986                                ) * strength_modifier;
2987                                if !impulse.is_approx_zero() {
2988                                    emitters.emit(KnockbackEvent {
2989                                        entity: effect_target,
2990                                        impulse,
2991                                    });
2992                                }
2993                            },
2994                            CombatEffect::EnergyReward(ec) => {
2995                                emitters.emit(EnergyChangeEvent {
2996                                    entity: effect_target,
2997                                    change: ec
2998                                        * combat::compute_energy_reward_mod(
2999                                            data.inventories.get(effect_target),
3000                                            &data.msm,
3001                                        )
3002                                        * strength_modifier
3003                                        * data
3004                                            .stats
3005                                            .get(effect_target)
3006                                            .map_or(1.0, |s| s.energy_reward_modifier),
3007                                    reset_rate: false,
3008                                });
3009                            },
3010                            CombatEffect::Buff(b) => {
3011                                if rng.random::<f32>() < b.chance {
3012                                    emitters.emit(BuffEvent {
3013                                        entity: effect_target,
3014                                        buff_change: buff::BuffChange::Add(b.to_buff(
3015                                            *data.time,
3016                                            (
3017                                                data.uids.get(ev.entity).copied(),
3018                                                data.masses.get(ev.entity),
3019                                                None,
3020                                            ),
3021                                            (
3022                                                data.stats.get(effect_target),
3023                                                data.masses.get(effect_target),
3024                                            ),
3025                                            ev.damage_dealt,
3026                                            strength_modifier,
3027                                        )),
3028                                    });
3029                                }
3030                            },
3031                            CombatEffect::Lifesteal(l) => {
3032                                let change = HealthChange {
3033                                    amount: ev.damage_dealt * l * strength_modifier,
3034                                    by: dmg_contrib,
3035                                    cause: None,
3036                                    time: *data.time,
3037                                    precise: false,
3038                                    instance: rand::random(),
3039                                };
3040                                if change.amount.abs() > Health::HEALTH_EPSILON {
3041                                    emitters.emit(HealthChangeEvent {
3042                                        entity: effect_target,
3043                                        change,
3044                                    });
3045                                }
3046                            },
3047                            CombatEffect::Poise(p) => {
3048                                let change = -Poise::apply_poise_reduction(
3049                                    *p,
3050                                    data.inventories.get(effect_target),
3051                                    &data.msm,
3052                                    data.character_states.get(effect_target),
3053                                    data.stats.get(effect_target),
3054                                ) * strength_modifier
3055                                    * data
3056                                        .stats
3057                                        .get(ev.entity)
3058                                        .map_or(1.0, |s| s.poise_damage_modifier);
3059                                if change.abs() > Poise::POISE_EPSILON {
3060                                    let poise_change = PoiseChange {
3061                                        amount: change,
3062                                        impulse: *dir,
3063                                        by: dmg_contrib,
3064                                        cause: None,
3065                                        time: *data.time,
3066                                    };
3067                                    emitters.emit(PoiseChangeEvent {
3068                                        entity: effect_target,
3069                                        change: poise_change,
3070                                    });
3071                                }
3072                            },
3073                            CombatEffect::Heal(h) => {
3074                                let change = HealthChange {
3075                                    amount: *h * strength_modifier,
3076                                    by: dmg_contrib,
3077                                    cause: None,
3078                                    time: *data.time,
3079                                    precise: false,
3080                                    instance: rand::random(),
3081                                };
3082                                if change.amount.abs() > Health::HEALTH_EPSILON {
3083                                    emitters.emit(HealthChangeEvent {
3084                                        entity: effect_target,
3085                                        change,
3086                                    });
3087                                }
3088                            },
3089                            CombatEffect::Combo(c) => {
3090                                emitters.emit(ComboChangeEvent {
3091                                    entity: effect_target,
3092                                    change: (*c as f32 * strength_modifier).ceil() as i32,
3093                                });
3094                            },
3095                            CombatEffect::StageVulnerable(damage, section) => {
3096                                if data
3097                                    .character_states
3098                                    .get(effect_target)
3099                                    .is_some_and(|cs| cs.stage_section() == Some(*section))
3100                                {
3101                                    let change = HealthChange {
3102                                        amount: -ev.damage_dealt * damage * strength_modifier,
3103                                        by: dmg_contrib,
3104                                        cause: Some(DamageSource::Other),
3105                                        time: *data.time,
3106                                        precise: false,
3107                                        instance: rand::random(),
3108                                    };
3109                                    emitters.emit(HealthChangeEvent {
3110                                        entity: effect_target,
3111                                        change,
3112                                    });
3113                                }
3114                            },
3115                            CombatEffect::RefreshBuff(chance, b) => {
3116                                if rng.random::<f32>() < *chance {
3117                                    emitters.emit(BuffEvent {
3118                                        entity: effect_target,
3119                                        buff_change: buff::BuffChange::Refresh(*b),
3120                                    });
3121                                }
3122                            },
3123                            CombatEffect::BuffsVulnerable(damage, buff) => {
3124                                if data
3125                                    .buffs
3126                                    .get(effect_target)
3127                                    .is_some_and(|b| b.contains(*buff))
3128                                {
3129                                    let change = HealthChange {
3130                                        amount: -ev.damage_dealt * damage * strength_modifier,
3131                                        by: dmg_contrib,
3132                                        cause: Some(DamageSource::Other),
3133                                        time: *data.time,
3134                                        precise: false,
3135                                        instance: rand::random(),
3136                                    };
3137                                    emitters.emit(HealthChangeEvent {
3138                                        entity: effect_target,
3139                                        change,
3140                                    });
3141                                }
3142                            },
3143                            CombatEffect::StunnedVulnerable(damage) => {
3144                                if data
3145                                    .character_states
3146                                    .get(effect_target)
3147                                    .is_some_and(|cs| cs.is_stunned())
3148                                {
3149                                    let change = HealthChange {
3150                                        amount: -ev.damage_dealt * damage * strength_modifier,
3151                                        by: dmg_contrib,
3152                                        cause: Some(DamageSource::Other),
3153                                        time: *data.time,
3154                                        precise: false,
3155                                        instance: rand::random(),
3156                                    };
3157                                    emitters.emit(HealthChangeEvent {
3158                                        entity: effect_target,
3159                                        change,
3160                                    });
3161                                }
3162                            },
3163                            CombatEffect::SelfBuff(b) => {
3164                                if rng.random::<f32>() < b.chance {
3165                                    emitters.emit(BuffEvent {
3166                                        entity: effect_target,
3167                                        buff_change: buff::BuffChange::Add(b.to_self_buff(
3168                                            *data.time,
3169                                            (
3170                                                data.uids.get(effect_target).copied(),
3171                                                data.stats.get(effect_target),
3172                                                data.masses.get(effect_target),
3173                                                None,
3174                                            ),
3175                                            ev.damage_dealt,
3176                                            strength_modifier,
3177                                        )),
3178                                    });
3179                                }
3180                            },
3181                            CombatEffect::Energy(e) => {
3182                                emitters.emit(EnergyChangeEvent {
3183                                    entity: effect_target,
3184                                    change: *e * strength_modifier,
3185                                    reset_rate: true,
3186                                });
3187                            },
3188                            CombatEffect::Transform {
3189                                entity_spec,
3190                                allow_players,
3191                            } => {
3192                                if (data.players.get(effect_target).is_none() || *allow_players)
3193                                    && let Some(tgt_uid) = data.uids.get(effect_target)
3194                                {
3195                                    emitters.emit(TransformEvent {
3196                                        target_entity: *tgt_uid,
3197                                        entity_info: {
3198                                            let Ok(entity_config) = Ron::<EntityConfig>::load(
3199                                                entity_spec,
3200                                            )
3201                                            .inspect_err(|error| {
3202                                                error!(
3203                                                    ?entity_spec,
3204                                                    ?error,
3205                                                    "Could not load entity configuration for \
3206                                                     death effect"
3207                                                )
3208                                            }) else {
3209                                                continue;
3210                                            };
3211
3212                                            EntityInfo::at(
3213                                                data.positions
3214                                                    .get(effect_target)
3215                                                    .map(|p| p.0)
3216                                                    .unwrap_or_default(),
3217                                            )
3218                                            .with_entity_config(
3219                                                entity_config.read().clone().into_inner(),
3220                                                Some(entity_spec),
3221                                                &mut rng,
3222                                                None,
3223                                            )
3224                                        },
3225                                        allow_players: *allow_players,
3226                                        delete_on_failure: false,
3227                                    });
3228                                }
3229                            },
3230                            CombatEffect::DebuffsVulnerable {
3231                                mult,
3232                                scaling,
3233                                filter_attacker,
3234                                filter_weapon,
3235                            } => {
3236                                if let Some(buffs) = data.buffs.get(effect_target) {
3237                                    let num_debuffs = buffs.iter_active().flatten().filter(|b| {
3238                                        let debuff_filter = matches!(b.kind.differentiate(), buff::BuffDescriptor::SimpleNegative);
3239                                        let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == other_entity.and_then(|e| data.uids.get(e)).copied());
3240                                        let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
3241                                        debuff_filter && attacker_filter && weapon_filter
3242                                    }).count();
3243                                    if num_debuffs > 0 {
3244                                        let change = HealthChange {
3245                                            amount: -ev.damage_dealt
3246                                                * scaling.factor(num_debuffs as f32, 1.0)
3247                                                * mult
3248                                                * strength_modifier,
3249                                            by: dmg_contrib,
3250                                            cause: Some(DamageSource::Other),
3251                                            time: *data.time,
3252                                            precise: false,
3253                                            instance: rand::random(),
3254                                        };
3255                                        emitters.emit(HealthChangeEvent {
3256                                            entity: effect_target,
3257                                            change,
3258                                        });
3259                                    }
3260                                }
3261                            },
3262                        }
3263                    }
3264                }
3265            }
3266        }
3267    }
3268}
3269
3270impl ServerEvent for ChangeAbilityEvent {
3271    type SystemData<'a> = (
3272        WriteStorage<'a, comp::ActiveAbilities>,
3273        ReadStorage<'a, Inventory>,
3274        ReadStorage<'a, SkillSet>,
3275    );
3276
3277    fn handle(
3278        events: impl ExactSizeIterator<Item = Self>,
3279        (mut active_abilities, inventories, skill_sets): Self::SystemData<'_>,
3280    ) {
3281        for ev in events {
3282            if let Some(mut active_abilities) = active_abilities.get_mut(ev.entity) {
3283                active_abilities.change_ability(
3284                    ev.slot,
3285                    ev.auxiliary_key,
3286                    ev.new_ability,
3287                    inventories.get(ev.entity),
3288                    skill_sets.get(ev.entity),
3289                );
3290            }
3291        }
3292    }
3293}
3294
3295impl ServerEvent for UpdateMapMarkerEvent {
3296    type SystemData<'a> = (
3297        Entities<'a>,
3298        WriteStorage<'a, comp::MapMarker>,
3299        ReadStorage<'a, Group>,
3300        ReadStorage<'a, Uid>,
3301        ReadStorage<'a, Client>,
3302        ReadStorage<'a, Alignment>,
3303    );
3304
3305    fn handle(
3306        events: impl ExactSizeIterator<Item = Self>,
3307        (entities, mut map_markers, groups, uids, clients, alignments): Self::SystemData<'_>,
3308    ) {
3309        for ev in events {
3310            match ev.update {
3311                comp::MapMarkerChange::Update(waypoint) => {
3312                    let _ = map_markers.insert(ev.entity, comp::MapMarker(waypoint));
3313                },
3314                comp::MapMarkerChange::Remove => {
3315                    map_markers.remove(ev.entity);
3316                },
3317            }
3318            // Send updated waypoint to group members
3319            if let Some((group_id, uid)) = (&groups, &uids).lend_join().get(ev.entity, &entities) {
3320                for client in
3321                    comp::group::members(*group_id, &groups, &entities, &alignments, &uids)
3322                        .filter_map(|(e, _)| if e != ev.entity { clients.get(e) } else { None })
3323                {
3324                    client.send_fallible(ServerGeneral::MapMarker(
3325                        comp::MapMarkerUpdate::GroupMember(*uid, ev.update),
3326                    ));
3327                }
3328            }
3329        }
3330    }
3331}
3332
3333impl ServerEvent for MakeAdminEvent {
3334    type SystemData<'a> = (WriteStorage<'a, comp::Admin>, ReadStorage<'a, Player>);
3335
3336    fn handle(
3337        events: impl ExactSizeIterator<Item = Self>,
3338        (mut admins, players): Self::SystemData<'_>,
3339    ) {
3340        for ev in events {
3341            if players
3342                .get(ev.entity)
3343                .is_some_and(|player| player.uuid() == ev.uuid)
3344            {
3345                let _ = admins.insert(ev.entity, ev.admin);
3346            }
3347        }
3348    }
3349}
3350
3351impl ServerEvent for ChangeStanceEvent {
3352    type SystemData<'a> = WriteStorage<'a, comp::Stance>;
3353
3354    fn handle(events: impl ExactSizeIterator<Item = Self>, mut stances: Self::SystemData<'_>) {
3355        for ev in events {
3356            if let Some(mut stance) = stances.get_mut(ev.entity) {
3357                *stance = ev.stance;
3358            }
3359        }
3360    }
3361}
3362
3363impl ServerEvent for ChangeBodyEvent {
3364    type SystemData<'a> = (
3365        WriteExpect<'a, CharacterUpdater>,
3366        WriteStorage<'a, comp::Body>,
3367        WriteStorage<'a, comp::Mass>,
3368        WriteStorage<'a, comp::Density>,
3369        WriteStorage<'a, comp::Collider>,
3370        WriteStorage<'a, comp::Stats>,
3371        ReadStorage<'a, comp::Player>,
3372        ReadStorage<'a, comp::Presence>,
3373    );
3374
3375    fn handle(
3376        events: impl ExactSizeIterator<Item = Self>,
3377        (
3378            mut character_updater,
3379            mut bodies,
3380            mut masses,
3381            mut densities,
3382            mut colliders,
3383            mut stats,
3384            players,
3385            presences,
3386        ): Self::SystemData<'_>,
3387    ) {
3388        for ev in events {
3389            if let Some(mut body) = bodies.get_mut(ev.entity) {
3390                if let Some(permanent_change) = ev.permanent_change {
3391                    // If we aren't changing the right body, skip this change.
3392                    if permanent_change.expected_old_body != *body {
3393                        continue;
3394                    }
3395
3396                    if let Some(mut stats) = stats.get_mut(ev.entity)
3397                        && stats.original_body == permanent_change.expected_old_body
3398                    {
3399                        stats.original_body = ev.new_body;
3400                    }
3401
3402                    if let Some(player) = players.get(ev.entity)
3403                        && let Some(comp::Presence {
3404                            kind: comp::PresenceKind::Character(character_id),
3405                            ..
3406                        }) = presences.get(ev.entity)
3407                    {
3408                        character_updater.edit_character(
3409                            ev.entity,
3410                            player.uuid().to_string(),
3411                            *character_id,
3412                            None,
3413                            (ev.new_body,),
3414                            Some(permanent_change),
3415                        );
3416                    }
3417                }
3418
3419                *body = ev.new_body;
3420                masses
3421                    .insert(ev.entity, ev.new_body.mass())
3422                    .expect("We just got this entities body");
3423                densities
3424                    .insert(ev.entity, ev.new_body.density())
3425                    .expect("We just got this entities body");
3426                colliders
3427                    .insert(ev.entity, ev.new_body.collider())
3428                    .expect("We just got this entities body");
3429            }
3430        }
3431    }
3432}
3433
3434impl ServerEvent for RemoveLightEmitterEvent {
3435    type SystemData<'a> = WriteStorage<'a, comp::LightEmitter>;
3436
3437    fn handle(
3438        events: impl ExactSizeIterator<Item = Self>,
3439        mut light_emitters: Self::SystemData<'_>,
3440    ) {
3441        for ev in events {
3442            light_emitters.remove(ev.entity);
3443        }
3444    }
3445}
3446
3447impl ServerEvent for TeleportToPositionEvent {
3448    type SystemData<'a> = (
3449        Read<'a, IdMaps>,
3450        WriteStorage<'a, Is<VolumeRider>>,
3451        WriteStorage<'a, Pos>,
3452        WriteStorage<'a, comp::ForceUpdate>,
3453        ReadStorage<'a, Is<Rider>>,
3454        ReadStorage<'a, Presence>,
3455        ReadStorage<'a, Client>,
3456    );
3457
3458    fn handle(
3459        events: impl ExactSizeIterator<Item = Self>,
3460        (
3461            id_maps,
3462            mut is_volume_riders,
3463            mut positions,
3464            mut force_updates,
3465            is_riders,
3466            presences,
3467            clients,
3468        ): Self::SystemData<'_>,
3469    ) {
3470        for ev in events {
3471            if let Err(error) = crate::state_ext::position_mut(
3472                ev.entity,
3473                true,
3474                |pos| pos.0 = ev.position,
3475                &id_maps,
3476                &mut is_volume_riders,
3477                &mut positions,
3478                &mut force_updates,
3479                &is_riders,
3480                &presences,
3481                &clients,
3482            ) {
3483                warn!(?error, "Failed to teleport entity");
3484            }
3485        }
3486    }
3487}
3488
3489impl ServerEvent for StartTeleportingEvent {
3490    type SystemData<'a> = (
3491        Read<'a, Time>,
3492        WriteStorage<'a, comp::Teleporting>,
3493        ReadStorage<'a, Pos>,
3494        ReadStorage<'a, comp::Object>,
3495    );
3496
3497    fn handle(
3498        events: impl ExactSizeIterator<Item = Self>,
3499        (time, mut teleportings, positions, objects): Self::SystemData<'_>,
3500    ) {
3501        for ev in events {
3502            if let Some(end_time) = (!teleportings.contains(ev.entity))
3503                .then(|| positions.get(ev.entity))
3504                .flatten()
3505                .zip(positions.get(ev.portal))
3506                .filter(|(entity_pos, portal_pos)| {
3507                    entity_pos.0.distance_squared(portal_pos.0) <= TELEPORTER_RADIUS.powi(2)
3508                })
3509                .and_then(|(_, _)| {
3510                    Some(
3511                        time.0
3512                            + objects.get(ev.portal).and_then(|object| {
3513                                if let Object::Portal { buildup_time, .. } = object {
3514                                    Some(buildup_time.0)
3515                                } else {
3516                                    None
3517                                }
3518                            })?,
3519                    )
3520                })
3521            {
3522                let _ = teleportings.insert(ev.entity, comp::Teleporting {
3523                    portal: ev.portal,
3524                    end_time: Time(end_time),
3525                });
3526            }
3527        }
3528    }
3529}
3530
3531impl ServerEvent for RegrowHeadEvent {
3532    type SystemData<'a> = (
3533        Read<'a, EventBus<HealthChangeEvent>>,
3534        Read<'a, Time>,
3535        WriteStorage<'a, Heads>,
3536        ReadStorage<'a, Health>,
3537    );
3538
3539    fn handle(
3540        events: impl ExactSizeIterator<Item = Self>,
3541        (health_change_events, time, mut heads, healths): Self::SystemData<'_>,
3542    ) {
3543        let mut health_change_emitter = health_change_events.emitter();
3544        for ev in events {
3545            if let Some(mut heads) = heads.get_mut(ev.entity)
3546                && heads.regrow_oldest()
3547                && let Some(health) = healths.get(ev.entity)
3548            {
3549                let amount = 1.0 / (heads.capacity() as f32) * health.maximum();
3550                health_change_emitter.emit(HealthChangeEvent {
3551                    entity: ev.entity,
3552                    change: comp::HealthChange {
3553                        amount,
3554                        by: None,
3555                        cause: Some(DamageSource::Other),
3556                        time: *time,
3557                        precise: false,
3558                        instance: rand::random(),
3559                    },
3560                })
3561            }
3562        }
3563    }
3564}
3565
3566pub fn handle_transform(
3567    server: &mut Server,
3568    TransformEvent {
3569        target_entity,
3570        entity_info,
3571        allow_players,
3572        delete_on_failure,
3573    }: TransformEvent,
3574) {
3575    let Some(entity) = server.state().ecs().entity_from_uid(target_entity) else {
3576        return;
3577    };
3578
3579    if let Err(error) = transform_entity(server, entity, entity_info, allow_players) {
3580        if delete_on_failure
3581            && !server
3582                .state()
3583                .ecs()
3584                .read_storage::<Client>()
3585                .contains(entity)
3586        {
3587            _ = server.state.delete_entity_recorded(entity);
3588        }
3589
3590        error!(?error, ?target_entity, "Failed transform entity");
3591    }
3592}
3593
3594#[derive(Debug)]
3595pub enum TransformEntityError {
3596    EntityDead,
3597    UnexpectedSpecialEntity,
3598    LoadingCharacter,
3599    EntityIsPlayer,
3600}
3601
3602pub fn transform_entity(
3603    server: &mut Server,
3604    entity: Entity,
3605    entity_info: EntityInfo,
3606    allow_players: bool,
3607) -> Result<(), TransformEntityError> {
3608    let is_player = server
3609        .state()
3610        .read_storage::<comp::Player>()
3611        .contains(entity);
3612
3613    match SpawnEntityData::from_entity_info(entity_info) {
3614        SpawnEntityData::Npc(NpcData {
3615            inventory,
3616            stats,
3617            skill_set,
3618            poise,
3619            health,
3620            body,
3621            scale,
3622            agent,
3623            loot,
3624            alignment: _,
3625            pos: _,
3626            pets,
3627            rider,
3628            death_effects,
3629            rider_effects,
3630        }) => {
3631            fn set_or_remove_component<C: specs::Component>(
3632                server: &mut Server,
3633                entity: EcsEntity,
3634                component: Option<C>,
3635                with: Option<fn(&mut C, Option<C>)>,
3636            ) -> Result<(), TransformEntityError> {
3637                let mut storage = server.state.ecs_mut().write_storage::<C>();
3638
3639                if let Some(mut component) = component {
3640                    if let Some(with) = with {
3641                        let prev = storage.remove(entity);
3642                        with(&mut component, prev);
3643                    }
3644
3645                    storage
3646                        .insert(entity, component)
3647                        .and(Ok(()))
3648                        .map_err(|_| TransformEntityError::EntityDead)
3649                } else {
3650                    storage.remove(entity);
3651                    Ok(())
3652                }
3653            }
3654
3655            // Disable persistence
3656            'persist: {
3657                match server
3658                    .state
3659                    .ecs()
3660                    .read_storage::<Presence>()
3661                    .get(entity)
3662                    .map(|presence| presence.kind)
3663                {
3664                    // Transforming while the character is being loaded or is spectating is invalid!
3665                    Some(PresenceKind::Spectator | PresenceKind::LoadingCharacter(_)) => {
3666                        return Err(TransformEntityError::LoadingCharacter);
3667                    },
3668                    Some(PresenceKind::Character(_)) if !allow_players => {
3669                        return Err(TransformEntityError::EntityIsPlayer);
3670                    },
3671                    Some(PresenceKind::Possessor | PresenceKind::Character(_)) => {},
3672                    None => break 'persist,
3673                }
3674
3675                // Run persistence once before disabling it
3676                //
3677                // We must NOT early return between persist_entity() being called and
3678                // persistence being set to Possessor
3679                super::player::persist_entity(server.state_mut(), entity);
3680
3681                // We re-fetch presence here as mutable, because checking for a valid
3682                // [`PresenceKind`] must be done BEFORE persist_entity but persist_entity needs
3683                // exclusive mutable access to the server's state
3684                let mut presences = server.state.ecs().write_storage::<Presence>();
3685                let Some(presence) = presences.get_mut(entity) else {
3686                    // Checked above
3687                    unreachable!("We already know this entity has a Presence");
3688                };
3689
3690                if let PresenceKind::Character(id) = presence.kind {
3691                    server.state.ecs().write_resource::<IdMaps>().remove_entity(
3692                        Some(entity),
3693                        None,
3694                        Some(id),
3695                        None,
3696                    );
3697
3698                    presence.kind = PresenceKind::Possessor;
3699                }
3700            }
3701
3702            // Should do basically what StateExt::create_npc does
3703            set_or_remove_component(server, entity, Some(inventory), None)?;
3704            set_or_remove_component(server, entity, Some(stats), None)?;
3705            set_or_remove_component(server, entity, Some(skill_set), None)?;
3706            set_or_remove_component(server, entity, Some(poise), None)?;
3707            set_or_remove_component(server, entity, health, None)?;
3708            set_or_remove_component(server, entity, Some(comp::Energy::new(body)), None)?;
3709            set_or_remove_component(server, entity, Some(body), None)?;
3710            set_or_remove_component(server, entity, Some(body.mass()), None)?;
3711            set_or_remove_component(server, entity, Some(body.density()), None)?;
3712            set_or_remove_component(server, entity, Some(body.collider()), None)?;
3713            set_or_remove_component(server, entity, Some(scale), None)?;
3714            set_or_remove_component(server, entity, death_effects, None)?;
3715            set_or_remove_component(server, entity, rider_effects, None)?;
3716            // Reset active abilities
3717            set_or_remove_component(
3718                server,
3719                entity,
3720                Some(if body.is_humanoid() {
3721                    comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT)
3722                } else {
3723                    comp::ActiveAbilities::default()
3724                }),
3725                None,
3726            )?;
3727            set_or_remove_component(server, entity, body.heads().map(Heads::new), None)?;
3728
3729            // Don't add Agent or ItemDrops to players
3730            if !is_player {
3731                set_or_remove_component(
3732                    server,
3733                    entity,
3734                    agent,
3735                    Some(|new_agent, old_agent| {
3736                        if let Some(old_agent) = old_agent {
3737                            new_agent.target = old_agent.target;
3738                            new_agent.awareness = old_agent.awareness;
3739                        }
3740                    }),
3741                )?;
3742                set_or_remove_component(
3743                    server,
3744                    entity,
3745                    loot.to_items().map(comp::ItemDrops),
3746                    None,
3747                )?;
3748            }
3749
3750            // Spawn pets
3751            let position = server.state.read_component_copied::<comp::Pos>(entity);
3752            if let Some(pos) = position {
3753                for (pet, offset) in pets
3754                    .into_iter()
3755                    .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
3756                {
3757                    let pet_entity = handle_create_npc(server, CreateNpcEvent {
3758                        pos: comp::Pos(pos.0 + offset),
3759                        ori: comp::Ori::from_unnormalized_vec(offset).unwrap_or_default(),
3760                        npc: pet,
3761                    });
3762
3763                    tame_pet(server.state.ecs(), pet_entity, entity);
3764                }
3765
3766                // Spawn rider
3767                if let Some(rider) = rider {
3768                    let rider_entity = handle_create_npc(server, CreateNpcEvent {
3769                        pos,
3770                        ori: comp::Ori::default(),
3771                        npc: rider.to_npc_builder().0,
3772                    });
3773                    let uids = server.state().ecs().read_storage::<Uid>();
3774                    let link = Mounting {
3775                        mount: *uids
3776                            .get(entity)
3777                            .expect("We just got the position of this entity"),
3778                        rider: *uids.get(rider_entity).expect("We just created this entity"),
3779                    };
3780                    drop(uids);
3781                    server
3782                        .state
3783                        .link(link)
3784                        .expect("We know these entities exist");
3785                }
3786            }
3787        },
3788        SpawnEntityData::Special(_, _) => {
3789            return Err(TransformEntityError::UnexpectedSpecialEntity);
3790        },
3791    }
3792
3793    Ok(())
3794}
3795
3796pub fn handle_start_interaction(
3797    server: &mut Server,
3798    StartInteractionEvent(interaction): StartInteractionEvent,
3799) {
3800    let i = interaction.interactor;
3801    let t = interaction.target;
3802    if let Err(e) = server.state.link(interaction) {
3803        debug!("Error trying to start interaction between {i:?} and {t:?}: {e:?}");
3804    }
3805}