veloren_common_systems/
pool.rs

1use common::{
2    GroupTarget,
3    combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
4    comp::{
5        Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Mass, Ori,
6        PhysicsState, Player, Pos, Scale, Stats, ability::Dodgeable, aura::EnteredAuras,
7        pool::Pool,
8    },
9    event::{
10        BuffEvent, ComboChangeEvent, DeleteEvent, EmitExt, EnergyChangeEvent,
11        EntityAttackedHookEvent, EventBus, HealthChangeEvent, KnockbackEvent, ParryHookEvent,
12        PoiseChangeEvent, TransformEvent,
13    },
14    event_emitters,
15    outcome::Outcome,
16    resources::Time,
17    terrain::TerrainGrid,
18    uid::{IdMaps, Uid},
19    util::Dir,
20    vol::ReadVol,
21};
22use common_ecs::{Job, Origin, Phase, System};
23use specs::{
24    Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, SystemData, WriteStorage, shred,
25};
26use vek::*;
27
28event_emitters! {
29    struct Events[Emitters] {
30        delete: DeleteEvent,
31        health_change: HealthChangeEvent,
32        energy_change: EnergyChangeEvent,
33        parry_hook: ParryHookEvent,
34        knockback: KnockbackEvent,
35        buff: BuffEvent,
36        poise_change: PoiseChangeEvent,
37        combo_change: ComboChangeEvent,
38        entity_attack_hook: EntityAttackedHookEvent,
39        transform: TransformEvent,
40    }
41}
42
43#[derive(SystemData)]
44pub struct ReadData<'a> {
45    entities: Entities<'a>,
46    events: Events<'a>,
47    time: Read<'a, Time>,
48
49    terrain: ReadExpect<'a, TerrainGrid>,
50    id_maps: Read<'a, IdMaps>,
51    groups: ReadStorage<'a, Group>,
52    uids: ReadStorage<'a, Uid>,
53    positions: ReadStorage<'a, Pos>,
54    healths: ReadStorage<'a, Health>,
55    bodies: ReadStorage<'a, Body>,
56    energies: ReadStorage<'a, Energy>,
57    combos: ReadStorage<'a, Combo>,
58    inventories: ReadStorage<'a, Inventory>,
59    stats: ReadStorage<'a, Stats>,
60    masses: ReadStorage<'a, Mass>,
61    orientations: ReadStorage<'a, Ori>,
62    character_states: ReadStorage<'a, CharacterState>,
63    buffs: ReadStorage<'a, Buffs>,
64    alignments: ReadStorage<'a, Alignment>,
65    players: ReadStorage<'a, Player>,
66    scales: ReadStorage<'a, Scale>,
67    entered_auras: ReadStorage<'a, EnteredAuras>,
68    outcomes: Read<'a, EventBus<Outcome>>,
69    physics_states: ReadStorage<'a, PhysicsState>,
70}
71
72#[derive(Default)]
73pub struct Sys;
74impl<'a> System<'a> for Sys {
75    type SystemData = (ReadData<'a>, WriteStorage<'a, Pool>);
76
77    const NAME: &'static str = "pool";
78    const ORIGIN: Origin = Origin::Common;
79    const PHASE: Phase = Phase::Create;
80
81    fn run(_job: &mut Job<Self>, (read_data, mut pools): Self::SystemData) {
82        let mut emitters = read_data.events.get_emitters();
83        let mut outcomes_emitter = read_data.outcomes.emitter();
84        let mut rng = rand::rng();
85
86        (&read_data.entities, &mut pools, &read_data.positions)
87            .lend_join()
88            .for_each(|(pool_entity, mut pool, pool_pos)| {
89                // Expire the pool after its duration.
90                if read_data.time.0 > pool.start_time.0 + pool.properties.duration.0 {
91                    emitters.emit(DeleteEvent(pool_entity));
92                    return;
93                }
94
95                // Only tick at the configured interval.
96                if read_data.time.0 < pool.last_tick.0 + pool.properties.tick_dur.0 {
97                    return;
98                }
99                pool.last_tick = *read_data.time;
100
101                let pool_owner = pool.owner.and_then(|uid| read_data.id_maps.uid_entity(uid));
102                let pool_group = pool_owner.and_then(|e| read_data.groups.get(e));
103
104                for (target, uid_b, pos_b, health_b, body_b) in (
105                    &read_data.entities,
106                    &read_data.uids,
107                    &read_data.positions,
108                    &read_data.healths,
109                    &read_data.bodies,
110                )
111                    .join()
112                {
113                    // Skip self and dead entities.
114                    if pool_entity == target || health_b.is_dead {
115                        continue;
116                    }
117
118                    let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
119                    let rad_b = body_b.max_radius() * scale_b;
120
121                    // Broad-phase distance check.
122                    if pool_pos.0.distance_squared(pos_b.0)
123                        > (pool.properties.radius + rad_b).powi(2)
124                    {
125                        continue;
126                    }
127
128                    // Line-of-sight check: cast a ray from the pool surface
129                    // (slightly elevated) to the target entity centre.  If
130                    // terrain fills the ray before we reach the target, skip.
131                    let ray_origin = pool_pos.0 + Vec3::unit_z() * 0.5;
132                    let tgt_dist = ray_origin.distance(pos_b.0);
133                    let ray_dist = read_data
134                        .terrain
135                        .ray(ray_origin, pos_b.0)
136                        .until(|b: &_| b.is_filled())
137                        .cast()
138                        .0;
139                    if ray_dist < tgt_dist * 0.9 {
140                        // Terrain occludes the target.
141                        continue;
142                    }
143
144                    let same_group = pool_group
145                        .map(|group_a| Some(group_a) == read_data.groups.get(target))
146                        .unwrap_or(Some(*uid_b) == pool.owner);
147
148                    let target_group = if same_group {
149                        GroupTarget::InGroup
150                    } else {
151                        GroupTarget::OutOfGroup
152                    };
153
154                    let allow_friendly_fire = pool_owner.is_some_and(|owner_entity| {
155                        combat::allow_friendly_fire(&read_data.entered_auras, owner_entity, target)
156                    });
157
158                    let dir = Dir::from_unnormalized(pos_b.0 - pool_pos.0).unwrap_or_default();
159
160                    let attacker_info =
161                        pool_owner
162                            .zip(pool.owner)
163                            .map(|(entity, uid)| AttackerInfo {
164                                entity,
165                                uid,
166                                group: read_data.groups.get(entity),
167                                energy: read_data.energies.get(entity),
168                                combo: read_data.combos.get(entity),
169                                inventory: read_data.inventories.get(entity),
170                                stats: read_data.stats.get(entity),
171                                mass: read_data.masses.get(entity),
172                                pos: Some(pool_pos.0),
173                            });
174
175                    let target_info = TargetInfo {
176                        entity: target,
177                        uid: *uid_b,
178                        inventory: read_data.inventories.get(target),
179                        stats: read_data.stats.get(target),
180                        health: Some(health_b),
181                        pos: pos_b.0,
182                        ori: read_data.orientations.get(target),
183                        char_state: read_data.character_states.get(target),
184                        energy: read_data.energies.get(target),
185                        buffs: read_data.buffs.get(target),
186                        mass: read_data.masses.get(target),
187                        player: read_data.players.get(target),
188                    };
189
190                    //TODO: Consider making pool hardcoded jump dodgeable only (like ground
191                    // shockwaves)
192                    let target_dodging = match pool.properties.dodgeable {
193                        Dodgeable::Roll => read_data
194                            .character_states
195                            .get(target)
196                            .and_then(|cs| cs.roll_attack_immunities())
197                            .is_some_and(|i| i.pools),
198                        Dodgeable::Jump => read_data
199                            .physics_states
200                            .get(target)
201                            .is_some_and(|ps| ps.on_ground.is_none()),
202                        Dodgeable::No => false,
203                    };
204
205                    let permit_pvp = combat::permit_pvp(
206                        &read_data.alignments,
207                        &read_data.players,
208                        &read_data.entered_auras,
209                        &read_data.id_maps,
210                        pool_owner,
211                        target,
212                    );
213
214                    let attack_options = AttackOptions {
215                        target_dodging,
216                        permit_pvp,
217                        allow_friendly_fire,
218                        target_group,
219                        precision_mult: None,
220                    };
221
222                    pool.properties.attack.apply_attack(
223                        attacker_info,
224                        &target_info,
225                        dir,
226                        attack_options,
227                        1.0,
228                        AttackSource::Pool,
229                        *read_data.time,
230                        &mut emitters,
231                        |o| outcomes_emitter.emit(o),
232                        &mut rng,
233                        0,
234                    );
235                }
236            });
237    }
238}