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