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