Skip to main content

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