veloren_server/events/
entity_creation.rs

1use crate::{
2    CharacterUpdater, Server, StateExt, client::Client, events::player::handle_exit_ingame,
3    persistence::PersistedComponents, pet::tame_pet, presence::RepositionToFreeSpace, sys,
4};
5use common::{
6    CachedSpatialGrid,
7    combat::AttackTarget,
8    comp::{
9        self, Alignment, BehaviorCapability, Body, Group, Inventory, ItemDrops, LightEmitter,
10        Object, Ori, Pos, ThrownItem, TradingBehavior, Vel, WaypointArea,
11        aura::{Aura, AuraKind, AuraTarget},
12        body,
13        buff::{BuffCategory, BuffData, BuffKind, BuffSource},
14        item::MaterialStatManifest,
15        ship::figuredata::VOXEL_COLLIDER_MANIFEST,
16        tool::AbilityMap,
17    },
18    consts::MAX_CAMPFIRE_RANGE,
19    event::{
20        ArcingEvent, CreateAuraEntityEvent, CreateItemDropEvent, CreateNpcEvent,
21        CreateNpcGroupEvent, CreateObjectEvent, CreateShipEvent, CreateSpecialEntityEvent,
22        EventBus, InitializeCharacterEvent, InitializeSpectatorEvent, NpcBuilder, ShockwaveEvent,
23        ShootEvent, SummonBeamPillarsEvent, ThrowEvent, UpdateCharacterDataEvent,
24    },
25    generation::SpecialEntity,
26    mounting::{Mounting, Volume, VolumeMounting, VolumePos},
27    outcome::Outcome,
28    resources::{Secs, Time},
29    terrain::TerrainGrid,
30    uid::{IdMaps, Uid},
31    util::Dir,
32    vol::IntoFullVolIterator,
33};
34use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
35use specs::{Builder, Entity as EcsEntity, WorldExt};
36use std::time::Duration;
37use vek::{Rgb, Vec3};
38
39use super::group_manip::update_map_markers;
40
41pub fn handle_initialize_character(server: &mut Server, ev: InitializeCharacterEvent) {
42    let updater = server.state.ecs().fetch::<CharacterUpdater>();
43    let pending_database_action = updater.has_pending_database_action(ev.character_id);
44    drop(updater);
45
46    if !pending_database_action {
47        let clamped_vds = ev
48            .requested_view_distances
49            .clamp(server.settings().max_view_distance);
50        server
51            .state
52            .initialize_character_data(ev.entity, ev.character_id, clamped_vds);
53        // Correct client if its requested VD is too high.
54        if ev.requested_view_distances.terrain != clamped_vds.terrain {
55            server.notify_client(
56                ev.entity,
57                ServerGeneral::SetViewDistance(clamped_vds.terrain),
58            );
59        }
60    } else {
61        // A character delete or update was somehow initiated after the login commenced,
62        // so kick the client out of "ingame" without saving any data and abort
63        // the character loading process.
64        handle_exit_ingame(server, ev.entity, true);
65    }
66}
67
68pub fn handle_initialize_spectator(server: &mut Server, ev: InitializeSpectatorEvent) {
69    let clamped_vds = ev.1.clamp(server.settings().max_view_distance);
70    server.state.initialize_spectator_data(ev.0, clamped_vds);
71    // Correct client if its requested VD is too high.
72    if ev.1.terrain != clamped_vds.terrain {
73        server.notify_client(ev.0, ServerGeneral::SetViewDistance(clamped_vds.terrain));
74    }
75    sys::subscription::initialize_region_subscription(server.state.ecs(), ev.0);
76}
77
78pub fn handle_loaded_character_data(server: &mut Server, ev: UpdateCharacterDataEvent) {
79    let loaded_components = PersistedComponents {
80        body: ev.components.0,
81        hardcore: ev.components.1,
82        stats: ev.components.2,
83        skill_set: ev.components.3,
84        inventory: ev.components.4,
85        waypoint: ev.components.5,
86        pets: ev.components.6,
87        active_abilities: ev.components.7,
88        map_marker: ev.components.8,
89    };
90    if let Some(marker) = loaded_components.map_marker {
91        server.notify_client(
92            ev.entity,
93            ServerGeneral::MapMarker(comp::MapMarkerUpdate::Owned(comp::MapMarkerChange::Update(
94                marker.0,
95            ))),
96        );
97    }
98
99    let result_msg = if let Err(err) = server
100        .state
101        .update_character_data(ev.entity, loaded_components)
102    {
103        handle_exit_ingame(server, ev.entity, false); // remove client from in-game state
104        ServerGeneral::CharacterDataLoadResult(Err(err))
105    } else {
106        sys::subscription::initialize_region_subscription(server.state.ecs(), ev.entity);
107        // We notify the client with the metadata result from the operation.
108        ServerGeneral::CharacterDataLoadResult(Ok(ev.metadata))
109    };
110    server.notify_client(ev.entity, result_msg);
111}
112
113pub fn handle_create_npc(server: &mut Server, ev: CreateNpcEvent) -> EcsEntity {
114    // Destruct the builder to ensure all fields are exhaustive
115    let NpcBuilder {
116        stats,
117        skill_set,
118        health,
119        poise,
120        inventory,
121        body,
122        mut agent,
123        alignment,
124        scale,
125        anchor,
126        loot,
127        pets,
128        rtsim_entity,
129        projectile,
130        heads,
131        death_effects,
132        rider_effects,
133        rider,
134    } = ev.npc;
135    let entity = server
136        .state
137        .create_npc(
138            ev.pos, ev.ori, stats, skill_set, health, poise, inventory, body, scale,
139        )
140        .maybe_with(heads)
141        .maybe_with(death_effects)
142        .maybe_with(rider_effects);
143
144    if let Some(agent) = &mut agent
145        && let Alignment::Owned(_) = &alignment
146    {
147        agent.behavior.allow(BehaviorCapability::TRADE);
148        agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
149    }
150
151    let entity = entity.with(alignment);
152
153    let entity = if let Some(agent) = agent {
154        entity.with(agent)
155    } else {
156        entity
157    };
158
159    let entity = if let Some(drop_items) = loot.to_items() {
160        entity.with(ItemDrops(drop_items))
161    } else {
162        entity
163    };
164
165    let entity = if let Some(home_chunk) = anchor {
166        entity.with(home_chunk)
167    } else {
168        entity
169    };
170
171    // Rtsim entity added to IdMaps below.
172    let entity = if let Some(rtsim_entity) = rtsim_entity {
173        entity.with(rtsim_entity).with(RepositionToFreeSpace {
174            needs_ground: false,
175            modify_waypoints: true,
176        })
177    } else {
178        entity
179    };
180
181    let entity = if let Some(projectile) = projectile {
182        entity.with(projectile)
183    } else {
184        entity
185    };
186
187    let new_entity = entity.build();
188
189    if let Some(rtsim_entity) = rtsim_entity {
190        server
191            .state()
192            .ecs()
193            .write_resource::<IdMaps>()
194            .add_rtsim(rtsim_entity, new_entity);
195    }
196
197    // Add to group system if a pet
198    if let comp::Alignment::Owned(owner_uid) = alignment {
199        let state = server.state();
200        let uids = state.ecs().read_storage::<Uid>();
201        let clients = state.ecs().read_storage::<Client>();
202        let mut group_manager = state.ecs().write_resource::<comp::group::GroupManager>();
203        if let Some(owner) = state.ecs().entity_from_uid(owner_uid) {
204            let map_markers = state.ecs().read_storage::<comp::MapMarker>();
205            group_manager.new_pet(
206                new_entity,
207                owner,
208                &mut state.ecs().write_storage(),
209                &state.ecs().entities(),
210                &state.ecs().read_storage(),
211                &uids,
212                &mut |entity, group_change| {
213                    clients
214                        .get(entity)
215                        .and_then(|c| {
216                            group_change
217                                .try_map_ref(|e| uids.get(*e).copied())
218                                .map(|g| (g, c))
219                        })
220                        .map(|(g, c)| {
221                            // Might be unnecessary, but maybe pets can somehow have map
222                            // markers in the future
223                            update_map_markers(&map_markers, &uids, c, &group_change);
224                            c.send_fallible(ServerGeneral::GroupUpdate(g));
225                        });
226                },
227            );
228        }
229    } else if let Some(group) = alignment.group() {
230        let _ = server.state.ecs().write_storage().insert(new_entity, group);
231    }
232
233    if let Some(rider) = rider {
234        let rider_entity = handle_create_npc(server, CreateNpcEvent {
235            pos: ev.pos,
236            ori: Ori::default(),
237            npc: *rider,
238        });
239        let uids = server.state().ecs().read_storage::<Uid>();
240        let link = Mounting {
241            mount: *uids.get(new_entity).expect("We just created this entity"),
242            rider: *uids.get(rider_entity).expect("We just created this entity"),
243        };
244        drop(uids);
245        server
246            .state
247            .link(link)
248            .expect("We just created these entities");
249    }
250
251    for (pet, offset) in pets {
252        let pet_entity = handle_create_npc(server, CreateNpcEvent {
253            pos: comp::Pos(ev.pos.0 + offset),
254            ori: Ori::from_unnormalized_vec(offset).unwrap_or_default(),
255            npc: pet,
256        });
257
258        tame_pet(server.state.ecs(), pet_entity, new_entity);
259    }
260
261    new_entity
262}
263
264pub fn handle_create_npc_group(server: &mut Server, ev: CreateNpcGroupEvent) {
265    let mut npcs = ev
266        .npcs
267        .into_iter()
268        .map(|ev| handle_create_npc(server, ev))
269        .collect::<Vec<_>>()
270        .into_iter();
271    let Some(leader) = npcs.next() else {
272        return;
273    };
274
275    let ecs = server.state().ecs();
276    let entities = ecs.entities();
277    let uids = ecs.read_storage::<Uid>();
278    let alignments = ecs.read_storage::<Alignment>();
279    let mut groups = ecs.write_storage::<Group>();
280    let mut group_manager = ecs.write_resource::<comp::group::GroupManager>();
281
282    if groups.get(leader).is_some() {
283        return;
284    }
285
286    for entity in npcs {
287        group_manager.add_group_member(
288            leader,
289            entity,
290            &entities,
291            &mut groups,
292            &alignments,
293            &uids,
294            |_, _| {},
295        );
296    }
297}
298
299pub fn handle_create_ship(server: &mut Server, ev: CreateShipEvent) {
300    let collider = ev.ship.make_collider();
301    let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
302
303    // TODO: Find better solution for this, maybe something like a serverside block
304    // of interests.
305    let (mut steering, mut _seats) = {
306        let mut steering = Vec::new();
307        let mut seats = Vec::new();
308
309        for (pos, block) in collider
310            .get_vol(&voxel_colliders_manifest)
311            .iter()
312            .flat_map(|voxel_collider| voxel_collider.volume().full_vol_iter())
313        {
314            match (block.is_controller(), block.is_mountable()) {
315                (true, true) => steering.push((pos, *block)),
316                (false, true) => seats.push((pos, *block)),
317                _ => {},
318            }
319        }
320        (steering.into_iter(), seats.into_iter())
321    };
322
323    let mut entity = server
324        .state
325        .create_ship(ev.pos, ev.ori, ev.ship, |_| collider);
326    /*
327    if let Some(mut agent) = agent {
328        let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
329        fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
330        agent =
331            agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z));
332        entity = entity.with(agent);
333    }
334    */
335    if let Some(rtsim_vehicle) = ev.rtsim_entity {
336        entity = entity.with(rtsim_vehicle);
337    }
338    let entity = entity.build();
339
340    if let Some(rtsim_entity) = ev.rtsim_entity {
341        server
342            .state()
343            .ecs()
344            .write_resource::<IdMaps>()
345            .add_rtsim(rtsim_entity, entity);
346    }
347
348    if let Some(driver) = ev.driver {
349        let npc_entity = handle_create_npc(server, CreateNpcEvent {
350            pos: ev.pos,
351            ori: ev.ori,
352            npc: driver,
353        });
354
355        let uids = server.state.ecs().read_storage::<Uid>();
356        let (rider_uid, mount_uid) = uids
357            .get(npc_entity)
358            .copied()
359            .zip(uids.get(entity).copied())
360            .expect("Couldn't get Uid from newly created ship and npc");
361        drop(uids);
362
363        if let Some((steering_pos, steering_block)) = steering.next() {
364            server
365                .state
366                .link(VolumeMounting {
367                    pos: VolumePos {
368                        kind: Volume::Entity(mount_uid),
369                        pos: steering_pos,
370                    },
371                    block: steering_block,
372                    rider: rider_uid,
373                })
374                .expect("Failed to link driver to ship");
375        } else {
376            server
377                .state
378                .link(Mounting {
379                    mount: mount_uid,
380                    rider: rider_uid,
381                })
382                .expect("Failed to link driver to ship");
383        }
384    }
385
386    /*
387    for passenger in ev.passengers {
388        let npc_entity = handle_create_npc(server, CreateNpcEvent {
389            pos: Pos(ev.pos.0 + Vec3::unit_z() * 5.0),
390            ori: ev.ori,
391            npc: passenger,
392            rider: None,
393        });
394        if let Some((rider_pos, rider_block)) = seats.next() {
395            let uids = server.state.ecs().read_storage::<Uid>();
396            let (rider_uid, mount_uid) = uids
397                .get(npc_entity)
398                .copied()
399                .zip(uids.get(entity).copied())
400                .expect("Couldn't get Uid from newly created ship and npc");
401            drop(uids);
402
403            server
404                .state
405                .link(VolumeMounting {
406                    pos: VolumePos {
407                        kind: Volume::Entity(mount_uid),
408                        pos: rider_pos,
409                    },
410                    block: rider_block,
411                    rider: rider_uid,
412                })
413                .expect("Failed to link passanger to ship");
414        }
415    }
416    */
417}
418
419pub fn handle_shoot(server: &mut Server, ev: ShootEvent) {
420    let state = server.state_mut();
421
422    let pos = ev.pos.0;
423
424    let vel = *ev.dir * ev.speed + ev.source_vel.map_or(Vec3::zero(), |v| v.0);
425
426    // Add an outcome
427    state
428        .ecs()
429        .read_resource::<EventBus<Outcome>>()
430        .emit_now(Outcome::ProjectileShot {
431            pos,
432            body: ev.body,
433            vel,
434        });
435
436    state
437        .create_projectile(Pos(pos), Vel(vel), ev.body, ev.projectile)
438        .maybe_with(ev.light)
439        .maybe_with(ev.object)
440        .maybe_with(ev.marker)
441        .build();
442}
443
444pub fn handle_throw(server: &mut Server, ev: ThrowEvent) {
445    let state = server.state_mut();
446
447    let thrown_item = state
448        .ecs()
449        .write_storage::<Inventory>()
450        .get_mut(ev.entity)
451        .and_then(|mut inv| {
452            if let Some(thrown_item) = inv.equipped(ev.equip_slot) {
453                let ability_map = state.ecs().read_resource::<AbilityMap>();
454                let msm = state.ecs().read_resource::<MaterialStatManifest>();
455                let time = state.ecs().read_resource::<Time>();
456
457                // If stackable, try to remove the throwable from inv stacks before
458                // removing the equipped one to avoid having to reequip after each throw
459                if let Some(inv_slot) = inv.get_slot_of_item(thrown_item)
460                    && thrown_item.is_stackable()
461                {
462                    inv.take(inv_slot, &ability_map, &msm)
463                } else {
464                    inv.replace_loadout_item(ev.equip_slot, None, *time)
465                }
466            } else {
467                None
468            }
469        })
470        .map(|mut thrown_item| {
471            thrown_item.put_in_world();
472            ThrownItem(thrown_item)
473        });
474
475    if let Some(thrown_item) = thrown_item {
476        let body = Body::Item(body::item::Body::from(&thrown_item));
477
478        let pos = ev.pos.0;
479
480        let vel = *ev.dir * ev.speed
481            + state
482                .ecs()
483                .read_storage::<Vel>()
484                .get(ev.entity)
485                .map_or(Vec3::zero(), |v| v.0);
486
487        // Add an outcome
488        state
489            .ecs()
490            .read_resource::<EventBus<Outcome>>()
491            .emit_now(Outcome::ProjectileShot { pos, body, vel });
492
493        state
494            .create_projectile(Pos(pos), Vel(vel), body, ev.projectile)
495            .with(thrown_item)
496            .maybe_with(ev.light)
497            .maybe_with(ev.object)
498            .build();
499    }
500}
501
502pub fn handle_shockwave(server: &mut Server, ev: ShockwaveEvent) {
503    let state = server.state_mut();
504    state
505        .create_shockwave(ev.properties, ev.pos, ev.ori)
506        .build();
507}
508
509pub fn handle_arc(server: &mut Server, ev: ArcingEvent) {
510    let state = server.state_mut();
511    state
512        .create_arcing(ev.arc, ev.target, ev.owner, ev.pos)
513        .build();
514}
515
516pub fn handle_create_special_entity(server: &mut Server, ev: CreateSpecialEntityEvent) {
517    let time = server.state.get_time();
518
519    match ev.entity {
520        SpecialEntity::Waypoint => {
521            server
522                .state
523                .create_object(Pos(ev.pos), comp::object::Body::CampfireLit)
524                .with(LightEmitter {
525                    col: Rgb::new(1.0, 0.3, 0.1),
526                    strength: 5.0,
527                    flicker: 1.0,
528                    animated: true,
529                    dir: None,
530                })
531                .with(WaypointArea::default())
532                .with(comp::Immovable)
533                .with(comp::EnteredAuras::default())
534                .with(comp::Auras::new(vec![
535                    Aura::new(
536                        AuraKind::Buff {
537                            kind: BuffKind::RestingHeal,
538                            data: BuffData::new(0.02, Some(Secs(1.0))),
539                            category: BuffCategory::Natural,
540                            source: BuffSource::World,
541                        },
542                        MAX_CAMPFIRE_RANGE,
543                        None,
544                        AuraTarget::All,
545                        Time(time),
546                    ),
547                    Aura::new(
548                        AuraKind::Buff {
549                            kind: BuffKind::Burning,
550                            data: BuffData::new(2.0, Some(Secs(10.0))),
551                            category: BuffCategory::Natural,
552                            source: BuffSource::World,
553                        },
554                        0.7,
555                        None,
556                        AuraTarget::All,
557                        Time(time),
558                    ),
559                ]))
560                .build();
561        },
562        SpecialEntity::Teleporter(portal) => {
563            server
564                .state
565                .create_teleporter(comp::Pos(ev.pos), portal)
566                .build();
567        },
568        SpecialEntity::ArenaTotem { range } => {
569            server
570                .state
571                .create_object(Pos(ev.pos), comp::object::Body::GnarlingTotemGreen)
572                .with(comp::Immovable)
573                .with(comp::EnteredAuras::default())
574                .with(comp::Auras::new(vec![
575                    Aura::new(
576                        AuraKind::FriendlyFire,
577                        range,
578                        None,
579                        AuraTarget::All,
580                        Time(time),
581                    ),
582                    Aura::new(AuraKind::ForcePvP, range, None, AuraTarget::All, Time(time)),
583                ]))
584                .build();
585        },
586    }
587}
588
589pub fn handle_create_item_drop(server: &mut Server, ev: CreateItemDropEvent) {
590    server
591        .state
592        .create_item_drop(ev.pos, ev.ori, ev.vel, ev.item, ev.loot_owner);
593}
594
595pub fn handle_create_object(
596    server: &mut Server,
597    CreateObjectEvent {
598        pos,
599        vel,
600        body,
601        object,
602        item,
603        light_emitter,
604        stats,
605    }: CreateObjectEvent,
606) {
607    match object {
608        Some(
609            object @ Object::Crux {
610                owner,
611                scale,
612                range,
613                strength,
614                duration,
615                ..
616            },
617        ) => {
618            let state = server.state_mut();
619            let time = *state.ecs().read_resource::<Time>();
620
621            // HACK: Spawn slightly damaged so that the health bar is visible and players
622            // are aware it is a killable entity
623            let mut health = comp::Health::new(Body::Object(body));
624            health.set_fraction(0.99996);
625
626            let crux = state
627                .create_object(pos, body)
628                .with(object)
629                .maybe_with(light_emitter)
630                .maybe_with(stats)
631                .with(comp::Scale(scale))
632                .with(health)
633                .with(comp::Energy::new(Body::Object(body)))
634                .with(comp::Poise::new(Body::Object(body)))
635                .with(comp::SkillSet::default())
636                .with(comp::Buffs::default())
637                .with(comp::Inventory::with_empty())
638                .with(comp::Immovable)
639                .with(comp::Auras::new(vec![Aura::new(
640                    AuraKind::Buff {
641                        kind: BuffKind::Heatstroke,
642                        data: BuffData {
643                            strength,
644                            duration: Some(duration),
645                            delay: None,
646                            secondary_duration: None,
647                            misc_data: None,
648                        },
649                        category: BuffCategory::Magical,
650                        source: BuffSource::World,
651                    },
652                    range,
653                    None,
654                    AuraTarget::NotGroupOf(owner),
655                    time,
656                )]))
657                .with(comp::projectile::ProjectileHitEntities::default())
658                .build();
659
660            if let Some(owner) = state.ecs().read_resource::<IdMaps>().uid_entity(owner) {
661                let mut group_manager = state.ecs().write_resource::<comp::group::GroupManager>();
662                group_manager.new_pet(
663                    crux,
664                    owner,
665                    &mut state.ecs().write_storage(),
666                    &state.ecs().entities(),
667                    &state.ecs().read_storage(),
668                    &state.ecs().read_storage::<Uid>(),
669                    &mut |_, _| {},
670                );
671            }
672        },
673        _ => {
674            server
675                .state
676                .create_object(pos, body)
677                .with(vel)
678                .maybe_with(object)
679                .maybe_with(item)
680                .maybe_with(light_emitter)
681                .maybe_with(stats)
682                .build();
683        },
684    }
685}
686
687pub fn handle_create_aura_entity(server: &mut Server, ev: CreateAuraEntityEvent) {
688    let time = *server.state.ecs().read_resource::<Time>();
689    let mut entity = server
690        .state
691        .ecs_mut()
692        .create_entity_synced()
693        .with(ev.pos)
694        .with(comp::Vel(Vec3::zero()))
695        .with(comp::Ori::default())
696        .with(ev.auras)
697        .with(comp::Alignment::Owned(ev.creator_uid));
698
699    // If a duration is specified, create a projectile component for the entity
700    if let Some(dur) = ev.duration {
701        let object = comp::Object::DeleteAfter {
702            spawned_at: time,
703            timeout: Duration::from_secs_f64(dur.0),
704        };
705        entity = entity.with(object);
706    }
707    entity.build();
708}
709
710pub fn handle_summon_beam_pillars(server: &mut Server, ev: SummonBeamPillarsEvent) {
711    let ecs = server.state().ecs();
712
713    let Some((&Pos(center), &summoner_alignment)) = ecs
714        .read_storage::<Pos>()
715        .get(ev.summoner)
716        .zip(ecs.read_storage::<Alignment>().get(ev.summoner))
717    else {
718        return;
719    };
720
721    let summon_pillar = |server: &mut Server, pos: Vec3<f32>, spawned_at| {
722        let integer_pos = pos.map(|x| x as i32);
723        let ground_height = server
724            .state()
725            .ecs()
726            .read_resource::<TerrainGrid>()
727            .find_ground(integer_pos)
728            .z as f32;
729
730        // If the distance from the attempted spawn position and the nearest valid
731        // position is too far, avoid spawning the fire pillar to prevent
732        // ability usage in a cave from spawning pillars on the surface or other
733        // edge cases
734        if (ground_height - pos.z).abs() <= 16.0 {
735            let ecs = server.state_mut().ecs_mut();
736
737            let pillar = ecs
738                .create_entity_synced()
739                .with(Pos(pos.with_z(ground_height)))
740                .with(Ori::from(Dir::up()))
741                .with(comp::Object::BeamPillar {
742                    spawned_at,
743                    buildup_duration: ev.buildup_duration,
744                    attack_duration: ev.attack_duration,
745                    beam_duration: ev.beam_duration,
746                    radius: ev.radius,
747                    height: ev.height,
748                    damage: ev.damage,
749                    damage_effect: ev.damage_effect.clone(),
750                    dodgeable: ev.dodgeable,
751                    tick_rate: ev.tick_rate,
752                    specifier: ev.specifier,
753                    indicator_specifier: ev.indicator_specifier,
754                })
755                .build();
756
757            let mut group_manager = ecs.write_resource::<comp::group::GroupManager>();
758            group_manager.new_pet(
759                pillar,
760                ev.summoner,
761                &mut ecs.write_storage(),
762                &ecs.entities(),
763                &ecs.read_storage(),
764                &ecs.read_storage::<Uid>(),
765                &mut |_, _| {},
766            );
767        }
768    };
769
770    let spawned_at = *ecs.read_resource::<Time>();
771    match ev.target {
772        AttackTarget::AllInRange(range) => {
773            let enemy_positions = ecs
774                .read_resource::<CachedSpatialGrid>()
775                .0
776                .in_circle_aabr(center.xy(), range)
777                .filter(|entity| {
778                    ecs.read_storage::<Alignment>()
779                        .get(*entity)
780                        .is_some_and(|alignment| summoner_alignment.hostile_towards(*alignment))
781                })
782                .filter(|entity| {
783                    ecs.read_storage::<comp::Group>()
784                        .get(ev.summoner)
785                        .is_none_or(|summoner_group| {
786                            ecs.read_storage::<comp::Group>()
787                                .get(*entity)
788                                .is_none_or(|entity_group| summoner_group != entity_group)
789                        })
790                })
791                .filter_map(|nearby_enemy| {
792                    ecs.read_storage::<Pos>()
793                        .get(nearby_enemy)
794                        .map(|Pos(pos)| *pos)
795                })
796                .collect::<Vec<_>>();
797
798            for enemy_pos in enemy_positions.into_iter() {
799                summon_pillar(server, enemy_pos, spawned_at);
800            }
801        },
802        AttackTarget::Pos(pos) => {
803            summon_pillar(server, pos, spawned_at);
804        },
805        AttackTarget::Entity(entity) => {
806            let pos = ecs.read_storage::<Pos>().get(entity).map(|pos| pos.0);
807            if let Some(pos) = pos {
808                summon_pillar(server, pos, spawned_at);
809            }
810        },
811    }
812}