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