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::RepositionOnChunkLoad, sys,
4};
5use common::{
6    CachedSpatialGrid,
7    combat::AttackTarget,
8    comp::{
9        self, Alignment, BehaviorCapability, Body, Inventory, ItemDrops, LightEmitter, Object, Ori,
10        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        CreateAuraEntityEvent, CreateItemDropEvent, CreateNpcEvent, CreateObjectEvent,
21        CreateShipEvent, CreateSpecialEntityEvent, EventBus, InitializeCharacterEvent,
22        InitializeSpectatorEvent, NpcBuilder, ShockwaveEvent, ShootEvent, SummonBeamPillarsEvent,
23        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        if let Alignment::Owned(_) = &alignment {
146            agent.behavior.allow(BehaviorCapability::TRADE);
147            agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
148        }
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(RepositionOnChunkLoad {
174            needs_ground: false,
175        })
176    } else {
177        entity
178    };
179
180    let entity = if let Some(projectile) = projectile {
181        entity.with(projectile)
182    } else {
183        entity
184    };
185
186    let new_entity = entity.build();
187
188    if let Some(rtsim_entity) = rtsim_entity {
189        server
190            .state()
191            .ecs()
192            .write_resource::<IdMaps>()
193            .add_rtsim(rtsim_entity, new_entity);
194    }
195
196    // Add to group system if a pet
197    if let comp::Alignment::Owned(owner_uid) = alignment {
198        let state = server.state();
199        let uids = state.ecs().read_storage::<Uid>();
200        let clients = state.ecs().read_storage::<Client>();
201        let mut group_manager = state.ecs().write_resource::<comp::group::GroupManager>();
202        if let Some(owner) = state.ecs().entity_from_uid(owner_uid) {
203            let map_markers = state.ecs().read_storage::<comp::MapMarker>();
204            group_manager.new_pet(
205                new_entity,
206                owner,
207                &mut state.ecs().write_storage(),
208                &state.ecs().entities(),
209                &state.ecs().read_storage(),
210                &uids,
211                &mut |entity, group_change| {
212                    clients
213                        .get(entity)
214                        .and_then(|c| {
215                            group_change
216                                .try_map_ref(|e| uids.get(*e).copied())
217                                .map(|g| (g, c))
218                        })
219                        .map(|(g, c)| {
220                            // Might be unnecessary, but maybe pets can somehow have map
221                            // markers in the future
222                            update_map_markers(&map_markers, &uids, c, &group_change);
223                            c.send_fallible(ServerGeneral::GroupUpdate(g));
224                        });
225                },
226            );
227        }
228    } else if let Some(group) = alignment.group() {
229        let _ = server.state.ecs().write_storage().insert(new_entity, group);
230    }
231
232    if let Some(rider) = rider {
233        let rider_entity = handle_create_npc(server, CreateNpcEvent {
234            pos: ev.pos,
235            ori: Ori::default(),
236            npc: *rider,
237        });
238        let uids = server.state().ecs().read_storage::<Uid>();
239        let link = Mounting {
240            mount: *uids.get(new_entity).expect("We just created this entity"),
241            rider: *uids.get(rider_entity).expect("We just created this entity"),
242        };
243        drop(uids);
244        server
245            .state
246            .link(link)
247            .expect("We just created these entities");
248    }
249
250    for (pet, offset) in pets {
251        let pet_entity = handle_create_npc(server, CreateNpcEvent {
252            pos: comp::Pos(ev.pos.0 + offset),
253            ori: Ori::from_unnormalized_vec(offset).unwrap_or_default(),
254            npc: pet,
255        });
256
257        tame_pet(server.state.ecs(), pet_entity, new_entity);
258    }
259
260    new_entity
261}
262
263pub fn handle_create_ship(server: &mut Server, ev: CreateShipEvent) {
264    let collider = ev.ship.make_collider();
265    let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
266
267    // TODO: Find better solution for this, maybe something like a serverside block
268    // of interests.
269    let (mut steering, mut _seats) = {
270        let mut steering = Vec::new();
271        let mut seats = Vec::new();
272
273        for (pos, block) in collider
274            .get_vol(&voxel_colliders_manifest)
275            .iter()
276            .flat_map(|voxel_collider| voxel_collider.volume().full_vol_iter())
277        {
278            match (block.is_controller(), block.is_mountable()) {
279                (true, true) => steering.push((pos, *block)),
280                (false, true) => seats.push((pos, *block)),
281                _ => {},
282            }
283        }
284        (steering.into_iter(), seats.into_iter())
285    };
286
287    let mut entity = server
288        .state
289        .create_ship(ev.pos, ev.ori, ev.ship, |_| collider);
290    /*
291    if let Some(mut agent) = agent {
292        let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
293        fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
294        agent =
295            agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z));
296        entity = entity.with(agent);
297    }
298    */
299    if let Some(rtsim_vehicle) = ev.rtsim_entity {
300        entity = entity.with(rtsim_vehicle);
301    }
302    let entity = entity.build();
303
304    if let Some(rtsim_entity) = ev.rtsim_entity {
305        server
306            .state()
307            .ecs()
308            .write_resource::<IdMaps>()
309            .add_rtsim(rtsim_entity, entity);
310    }
311
312    if let Some(driver) = ev.driver {
313        let npc_entity = handle_create_npc(server, CreateNpcEvent {
314            pos: ev.pos,
315            ori: ev.ori,
316            npc: driver,
317        });
318
319        let uids = server.state.ecs().read_storage::<Uid>();
320        let (rider_uid, mount_uid) = uids
321            .get(npc_entity)
322            .copied()
323            .zip(uids.get(entity).copied())
324            .expect("Couldn't get Uid from newly created ship and npc");
325        drop(uids);
326
327        if let Some((steering_pos, steering_block)) = steering.next() {
328            server
329                .state
330                .link(VolumeMounting {
331                    pos: VolumePos {
332                        kind: Volume::Entity(mount_uid),
333                        pos: steering_pos,
334                    },
335                    block: steering_block,
336                    rider: rider_uid,
337                })
338                .expect("Failed to link driver to ship");
339        } else {
340            server
341                .state
342                .link(Mounting {
343                    mount: mount_uid,
344                    rider: rider_uid,
345                })
346                .expect("Failed to link driver to ship");
347        }
348    }
349
350    /*
351    for passenger in ev.passengers {
352        let npc_entity = handle_create_npc(server, CreateNpcEvent {
353            pos: Pos(ev.pos.0 + Vec3::unit_z() * 5.0),
354            ori: ev.ori,
355            npc: passenger,
356            rider: None,
357        });
358        if let Some((rider_pos, rider_block)) = seats.next() {
359            let uids = server.state.ecs().read_storage::<Uid>();
360            let (rider_uid, mount_uid) = uids
361                .get(npc_entity)
362                .copied()
363                .zip(uids.get(entity).copied())
364                .expect("Couldn't get Uid from newly created ship and npc");
365            drop(uids);
366
367            server
368                .state
369                .link(VolumeMounting {
370                    pos: VolumePos {
371                        kind: Volume::Entity(mount_uid),
372                        pos: rider_pos,
373                    },
374                    block: rider_block,
375                    rider: rider_uid,
376                })
377                .expect("Failed to link passanger to ship");
378        }
379    }
380    */
381}
382
383pub fn handle_shoot(server: &mut Server, ev: ShootEvent) {
384    let state = server.state_mut();
385
386    let pos = ev.pos.0;
387
388    let vel = *ev.dir * ev.speed
389        + ev.entity
390            .and_then(|entity| state.ecs().read_storage::<Vel>().get(entity).map(|v| v.0))
391            .unwrap_or(Vec3::zero());
392
393    // Add an outcome
394    state
395        .ecs()
396        .read_resource::<EventBus<Outcome>>()
397        .emit_now(Outcome::ProjectileShot {
398            pos,
399            body: ev.body,
400            vel,
401        });
402
403    state
404        .create_projectile(Pos(pos), Vel(vel), ev.body, ev.projectile)
405        .maybe_with(ev.light)
406        .maybe_with(ev.object)
407        .build();
408}
409
410pub fn handle_throw(server: &mut Server, ev: ThrowEvent) {
411    let state = server.state_mut();
412
413    let thrown_item = state
414        .ecs()
415        .write_storage::<Inventory>()
416        .get_mut(ev.entity)
417        .and_then(|mut inv| {
418            if let Some(thrown_item) = inv.equipped(ev.equip_slot) {
419                let ability_map = state.ecs().read_resource::<AbilityMap>();
420                let msm = state.ecs().read_resource::<MaterialStatManifest>();
421                let time = state.ecs().read_resource::<Time>();
422
423                // If stackable, try to remove the throwable from inv stacks before
424                // removing the equipped one to avoid having to reequip after each throw
425                if let Some(inv_slot) = inv.get_slot_of_item(thrown_item)
426                    && thrown_item.is_stackable()
427                {
428                    inv.take(inv_slot, &ability_map, &msm)
429                } else {
430                    inv.replace_loadout_item(ev.equip_slot, None, *time)
431                }
432            } else {
433                None
434            }
435        })
436        .map(|mut thrown_item| {
437            thrown_item.put_in_world();
438            ThrownItem(thrown_item)
439        });
440
441    if let Some(thrown_item) = thrown_item {
442        let body = Body::Item(body::item::Body::from(&thrown_item));
443
444        let pos = ev.pos.0;
445
446        let vel = *ev.dir * ev.speed
447            + state
448                .ecs()
449                .read_storage::<Vel>()
450                .get(ev.entity)
451                .map_or(Vec3::zero(), |v| v.0);
452
453        // Add an outcome
454        state
455            .ecs()
456            .read_resource::<EventBus<Outcome>>()
457            .emit_now(Outcome::ProjectileShot { pos, body, vel });
458
459        state
460            .create_projectile(Pos(pos), Vel(vel), body, ev.projectile)
461            .with(thrown_item)
462            .maybe_with(ev.light)
463            .maybe_with(ev.object)
464            .build();
465    }
466}
467
468pub fn handle_shockwave(server: &mut Server, ev: ShockwaveEvent) {
469    let state = server.state_mut();
470    state
471        .create_shockwave(ev.properties, ev.pos, ev.ori)
472        .build();
473}
474
475pub fn handle_create_special_entity(server: &mut Server, ev: CreateSpecialEntityEvent) {
476    let time = server.state.get_time();
477
478    match ev.entity {
479        SpecialEntity::Waypoint => {
480            server
481                .state
482                .create_object(Pos(ev.pos), comp::object::Body::CampfireLit)
483                .with(LightEmitter {
484                    col: Rgb::new(1.0, 0.3, 0.1),
485                    strength: 5.0,
486                    flicker: 1.0,
487                    animated: true,
488                })
489                .with(WaypointArea::default())
490                .with(comp::Immovable)
491                .with(comp::EnteredAuras::default())
492                .with(comp::Auras::new(vec![
493                    Aura::new(
494                        AuraKind::Buff {
495                            kind: BuffKind::RestingHeal,
496                            data: BuffData::new(0.02, Some(Secs(1.0))),
497                            category: BuffCategory::Natural,
498                            source: BuffSource::World,
499                        },
500                        MAX_CAMPFIRE_RANGE,
501                        None,
502                        AuraTarget::All,
503                        Time(time),
504                    ),
505                    Aura::new(
506                        AuraKind::Buff {
507                            kind: BuffKind::Burning,
508                            data: BuffData::new(2.0, Some(Secs(10.0))),
509                            category: BuffCategory::Natural,
510                            source: BuffSource::World,
511                        },
512                        0.7,
513                        None,
514                        AuraTarget::All,
515                        Time(time),
516                    ),
517                ]))
518                .build();
519        },
520        SpecialEntity::Teleporter(portal) => {
521            server
522                .state
523                .create_teleporter(comp::Pos(ev.pos), portal)
524                .build();
525        },
526        SpecialEntity::ArenaTotem { range } => {
527            server
528                .state
529                .create_object(Pos(ev.pos), comp::object::Body::GnarlingTotemGreen)
530                .with(comp::Immovable)
531                .with(comp::EnteredAuras::default())
532                .with(comp::Auras::new(vec![
533                    Aura::new(
534                        AuraKind::FriendlyFire,
535                        range,
536                        None,
537                        AuraTarget::All,
538                        Time(time),
539                    ),
540                    Aura::new(AuraKind::ForcePvP, range, None, AuraTarget::All, Time(time)),
541                ]))
542                .build();
543        },
544    }
545}
546
547pub fn handle_create_item_drop(server: &mut Server, ev: CreateItemDropEvent) {
548    server
549        .state
550        .create_item_drop(ev.pos, ev.ori, ev.vel, ev.item, ev.loot_owner);
551}
552
553pub fn handle_create_object(
554    server: &mut Server,
555    CreateObjectEvent {
556        pos,
557        vel,
558        body,
559        object,
560        item,
561        light_emitter,
562        stats,
563    }: CreateObjectEvent,
564) {
565    match object {
566        Some(
567            object @ Object::Crux {
568                owner,
569                scale,
570                range,
571                strength,
572                duration,
573                ..
574            },
575        ) => {
576            let state = server.state_mut();
577            let time = *state.ecs().read_resource::<Time>();
578
579            // HACK: Spawn slightly damaged so that the health bar is visible and players
580            // are aware it is a killable entity
581            let mut health = comp::Health::new(Body::Object(body));
582            health.set_fraction(0.99996);
583
584            let crux = state
585                .create_object(pos, body)
586                .with(object)
587                .maybe_with(light_emitter)
588                .maybe_with(stats)
589                .with(comp::Scale(scale))
590                .with(health)
591                .with(comp::Energy::new(Body::Object(body)))
592                .with(comp::Poise::new(Body::Object(body)))
593                .with(comp::SkillSet::default())
594                .with(comp::Buffs::default())
595                .with(comp::Inventory::with_empty())
596                .with(comp::Immovable)
597                .with(comp::Auras::new(vec![Aura::new(
598                    AuraKind::Buff {
599                        kind: BuffKind::Heatstroke,
600                        data: BuffData {
601                            strength,
602                            duration: Some(duration),
603                            delay: None,
604                            secondary_duration: None,
605                            misc_data: None,
606                        },
607                        category: BuffCategory::Magical,
608                        source: BuffSource::World,
609                    },
610                    range,
611                    None,
612                    AuraTarget::NotGroupOf(owner),
613                    time,
614                )]))
615                .build();
616
617            if let Some(owner) = state.ecs().read_resource::<IdMaps>().uid_entity(owner) {
618                let mut group_manager = state.ecs().write_resource::<comp::group::GroupManager>();
619                group_manager.new_pet(
620                    crux,
621                    owner,
622                    &mut state.ecs().write_storage(),
623                    &state.ecs().entities(),
624                    &state.ecs().read_storage(),
625                    &state.ecs().read_storage::<Uid>(),
626                    &mut |_, _| {},
627                );
628            }
629        },
630        _ => {
631            server
632                .state
633                .create_object(pos, body)
634                .with(vel)
635                .maybe_with(object)
636                .maybe_with(item)
637                .maybe_with(light_emitter)
638                .maybe_with(stats)
639                .build();
640        },
641    }
642}
643
644pub fn handle_create_aura_entity(server: &mut Server, ev: CreateAuraEntityEvent) {
645    let time = *server.state.ecs().read_resource::<Time>();
646    let mut entity = server
647        .state
648        .ecs_mut()
649        .create_entity_synced()
650        .with(ev.pos)
651        .with(comp::Vel(Vec3::zero()))
652        .with(comp::Ori::default())
653        .with(ev.auras)
654        .with(comp::Alignment::Owned(ev.creator_uid));
655
656    // If a duration is specified, create a projectile component for the entity
657    if let Some(dur) = ev.duration {
658        let object = comp::Object::DeleteAfter {
659            spawned_at: time,
660            timeout: Duration::from_secs_f64(dur.0),
661        };
662        entity = entity.with(object);
663    }
664    entity.build();
665}
666
667pub fn handle_summon_beam_pillars(server: &mut Server, ev: SummonBeamPillarsEvent) {
668    let ecs = server.state().ecs();
669
670    let Some((&Pos(center), &summoner_alignment)) = ecs
671        .read_storage::<Pos>()
672        .get(ev.summoner)
673        .zip(ecs.read_storage::<Alignment>().get(ev.summoner))
674    else {
675        return;
676    };
677
678    let summon_pillar = |server: &mut Server, pos: Vec3<f32>, spawned_at| {
679        let integer_pos = pos.map(|x| x as i32);
680        let ground_height = server
681            .state()
682            .ecs()
683            .read_resource::<TerrainGrid>()
684            .find_ground(integer_pos)
685            .z as f32;
686
687        // If the distance from the attempted spawn position and the nearest valid
688        // position is too far, avoid spawning the fire pillar to prevent
689        // ability usage in a cave from spawning pillars on the surface or other
690        // edge cases
691        if (ground_height - pos.z).abs() <= 16.0 {
692            let ecs = server.state_mut().ecs_mut();
693
694            let pillar = ecs
695                .create_entity_synced()
696                .with(Pos(pos.with_z(ground_height)))
697                .with(Ori::from(Dir::up()))
698                .with(comp::Object::BeamPillar {
699                    spawned_at,
700                    buildup_duration: ev.buildup_duration,
701                    attack_duration: ev.attack_duration,
702                    beam_duration: ev.beam_duration,
703                    radius: ev.radius,
704                    height: ev.height,
705                    damage: ev.damage,
706                    damage_effect: ev.damage_effect,
707                    dodgeable: ev.dodgeable,
708                    tick_rate: ev.tick_rate,
709                    specifier: ev.specifier,
710                    indicator_specifier: ev.indicator_specifier,
711                })
712                .build();
713
714            let mut group_manager = ecs.write_resource::<comp::group::GroupManager>();
715            group_manager.new_pet(
716                pillar,
717                ev.summoner,
718                &mut ecs.write_storage(),
719                &ecs.entities(),
720                &ecs.read_storage(),
721                &ecs.read_storage::<Uid>(),
722                &mut |_, _| {},
723            );
724        }
725    };
726
727    let spawned_at = *ecs.read_resource::<Time>();
728    match ev.target {
729        AttackTarget::AllInRange(range) => {
730            let enemy_positions = ecs
731                .read_resource::<CachedSpatialGrid>()
732                .0
733                .in_circle_aabr(center.xy(), range)
734                .filter(|entity| {
735                    ecs.read_storage::<Alignment>()
736                        .get(*entity)
737                        .is_some_and(|alignment| summoner_alignment.hostile_towards(*alignment))
738                })
739                .filter(|entity| {
740                    ecs.read_storage::<comp::Group>()
741                        .get(ev.summoner)
742                        .is_none_or(|summoner_group| {
743                            ecs.read_storage::<comp::Group>()
744                                .get(*entity)
745                                .is_none_or(|entity_group| summoner_group != entity_group)
746                        })
747                })
748                .filter_map(|nearby_enemy| {
749                    ecs.read_storage::<Pos>()
750                        .get(nearby_enemy)
751                        .map(|Pos(pos)| *pos)
752                })
753                .collect::<Vec<_>>();
754
755            for enemy_pos in enemy_positions.into_iter() {
756                summon_pillar(server, enemy_pos, spawned_at);
757            }
758        },
759        AttackTarget::Pos(pos) => {
760            summon_pillar(server, pos, spawned_at);
761        },
762        AttackTarget::Entity(entity) => {
763            let pos = ecs.read_storage::<Pos>().get(entity).map(|pos| pos.0);
764            if let Some(pos) = pos {
765                summon_pillar(server, pos, spawned_at);
766            }
767        },
768    }
769}