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