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