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