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