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 if read_data.time.0 > pool.start_time.0 + pool.properties.duration.0 {
91 emitters.emit(DeleteEvent(pool_entity));
92 return;
93 }
94
95 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 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 if pool_pos.0.distance_squared(pos_b.0)
123 > (pool.properties.radius + rad_b).powi(2)
124 {
125 continue;
126 }
127
128 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 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 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}