chunkedge_entity/
hitbox.rs

1#![allow(clippy::type_complexity)]
2
3use bevy_app::prelude::*;
4use bevy_ecs::prelude::*;
5use chunkedge_binary::IdOr;
6use chunkedge_math::{Aabb, UVec3, Vec3Swizzles};
7use chunkedge_protocol::Direction;
8use derive_more::Deref;
9
10use crate::*;
11
12#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
13pub struct HitboxShapeUpdateSet;
14
15#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
16pub struct HitboxComponentsAddSet;
17
18#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
19pub struct HitboxUpdateSet;
20
21pub struct HitboxPlugin;
22
23#[derive(Resource)]
24/// Settings for hitbox plugin
25pub struct EntityHitboxSettings {
26    /// Controls if a plugin should add hitbox component on each created entity.
27    /// Otherwise you should add hitbox component by yourself in order to use
28    /// it.
29    pub add_hitbox_component: bool,
30}
31
32impl Default for EntityHitboxSettings {
33    fn default() -> Self {
34        Self {
35            add_hitbox_component: true,
36        }
37    }
38}
39
40impl Plugin for HitboxPlugin {
41    fn build(&self, app: &mut App) {
42        app.init_resource::<EntityHitboxSettings>()
43            .configure_sets(PreUpdate, HitboxShapeUpdateSet)
44            .add_systems(
45                PreUpdate,
46                (
47                    update_constant_hitbox,
48                    update_warden_hitbox,
49                    update_area_effect_cloud_hitbox,
50                    update_armor_stand_hitbox,
51                    update_passive_child_hitbox,
52                    update_zombie_hitbox,
53                    update_piglin_hitbox,
54                    update_zoglin_hitbox,
55                    update_player_hitbox,
56                    update_item_frame_hitbox,
57                    update_slime_hitbox,
58                    update_painting_hitbox,
59                    update_shulker_hitbox,
60                ),
61            )
62            .configure_sets(PostUpdate, HitboxComponentsAddSet)
63            .add_systems(
64                PostUpdate,
65                add_hitbox_component.in_set(HitboxComponentsAddSet),
66            )
67            .configure_sets(PreUpdate, HitboxUpdateSet.after(HitboxShapeUpdateSet))
68            .add_systems(PreUpdate, update_hitbox.in_set(HitboxUpdateSet));
69    }
70}
71
72/// Size of hitbox. The only way to manipulate it without losing it on the next
73/// tick is using a marker entity. Marker entity's hitbox is never updated.
74#[derive(Component, Debug, PartialEq, Deref)]
75pub struct HitboxShape(pub Aabb);
76
77/// Hitbox, aabb of which is calculated each tick using its position and
78/// [`Hitbox`]. In order to change size of this hitbox you need to change
79/// [`Hitbox`].
80#[derive(Component, Debug, Deref)]
81pub struct Hitbox(Aabb);
82
83impl HitboxShape {
84    pub const ZERO: HitboxShape = HitboxShape(Aabb::ZERO);
85
86    pub fn get(&self) -> Aabb {
87        self.0
88    }
89
90    pub(crate) fn centered(&mut self, size: DVec3) {
91        self.0 = Aabb::from_bottom_size(DVec3::ZERO, size);
92    }
93
94    pub(crate) fn in_world(&self, pos: DVec3) -> Aabb {
95        self.0 + pos
96    }
97}
98
99impl Hitbox {
100    pub fn get(&self) -> Aabb {
101        self.0
102    }
103}
104
105fn add_hitbox_component(
106    settings: Res<EntityHitboxSettings>,
107    mut commands: Commands,
108    query: Query<(Entity, &Position), Added<entity::Entity>>,
109    alt_query: Query<(Entity, &Position, &HitboxShape), Added<HitboxShape>>,
110) {
111    if settings.add_hitbox_component {
112        for (entity, pos) in query.iter() {
113            commands
114                .entity(entity)
115                .insert(HitboxShape::ZERO)
116                .insert(Hitbox(HitboxShape::ZERO.in_world(pos.0)));
117        }
118    } else {
119        for (entity, pos, hitbox) in alt_query.iter() {
120            commands
121                .entity(entity)
122                .insert(Hitbox(hitbox.in_world(pos.0)));
123        }
124    }
125}
126
127fn update_hitbox(
128    mut hitbox_query: Query<
129        (&mut Hitbox, &HitboxShape, &Position),
130        Or<(Changed<HitboxShape>, Changed<Position>)>,
131    >,
132) {
133    for (mut in_world, hitbox, pos) in &mut hitbox_query {
134        in_world.0 = hitbox.in_world(pos.0);
135    }
136}
137
138fn update_constant_hitbox(
139    mut hitbox_query: Query<
140        (&mut HitboxShape, &EntityKind),
141        Or<(Changed<EntityKind>, Added<HitboxShape>)>,
142    >,
143) {
144    for (mut hitbox, entity_kind) in &mut hitbox_query {
145        let size = match *entity_kind {
146            EntityKind::ALLAY => [0.6, 0.35, 0.6],
147            EntityKind::CHEST_BOAT | EntityKind::BOAT => [1.375, 0.5625, 1.375],
148            EntityKind::FROG => [0.5, 0.5, 0.5],
149            EntityKind::TADPOLE => [0.4, 0.3, 0.4],
150            EntityKind::SPECTRAL_ARROW | EntityKind::ARROW => [0.5, 0.5, 0.5],
151            EntityKind::AXOLOTL => [1.3, 0.6, 1.3],
152            EntityKind::BAT => [0.5, 0.9, 0.5],
153            EntityKind::BLAZE => [0.6, 1.8, 0.6],
154            EntityKind::CAT => [0.6, 0.7, 0.6],
155            EntityKind::CAVE_SPIDER => [0.7, 0.5, 0.7],
156            EntityKind::COD => [0.5, 0.3, 0.5],
157            EntityKind::CREEPER => [0.6, 1.7, 0.6],
158            EntityKind::DOLPHIN => [0.9, 0.6, 0.9],
159            EntityKind::DRAGON_FIREBALL => [1.0, 1.0, 1.0],
160            EntityKind::ELDER_GUARDIAN => [1.9975, 1.9975, 1.9975],
161            EntityKind::END_CRYSTAL => [2.0, 2.0, 2.0],
162            EntityKind::ENDER_DRAGON => [16.0, 8.0, 16.0],
163            EntityKind::ENDERMAN => [0.6, 2.9, 0.6],
164            EntityKind::ENDERMITE => [0.4, 0.3, 0.4],
165            EntityKind::EVOKER => [0.6, 1.95, 0.6],
166            EntityKind::EVOKER_FANGS => [0.5, 0.8, 0.5],
167            EntityKind::EXPERIENCE_ORB => [0.5, 0.5, 0.5],
168            EntityKind::EYE_OF_ENDER => [0.25, 0.25, 0.25],
169            EntityKind::FALLING_BLOCK => [0.98, 0.98, 0.98],
170            EntityKind::FIREWORK_ROCKET => [0.25, 0.25, 0.25],
171            EntityKind::GHAST => [4.0, 4.0, 4.0],
172            EntityKind::GIANT => [3.6, 12.0, 3.6],
173            EntityKind::GLOW_SQUID | EntityKind::SQUID => [0.8, 0.8, 0.8],
174            EntityKind::GUARDIAN => [0.85, 0.85, 0.85],
175            EntityKind::ILLUSIONER => [0.6, 1.95, 0.6],
176            EntityKind::IRON_GOLEM => [1.4, 2.7, 1.4],
177            EntityKind::ITEM => [0.25, 0.25, 0.25],
178            EntityKind::FIREBALL => [1.0, 1.0, 1.0],
179            EntityKind::LEASH_KNOT => [0.375, 0.5, 0.375],
180            EntityKind::LIGHTNING /* | EntityKind::MARKER - marker hitbox */ => [0.0; 3],
181            EntityKind::LLAMA_SPIT => [0.25, 0.25, 0.25],
182            EntityKind::MINECART
183            | EntityKind::CHEST_MINECART
184            | EntityKind::TNT_MINECART
185            | EntityKind::HOPPER_MINECART
186            | EntityKind::FURNACE_MINECART
187            | EntityKind::SPAWNER_MINECART
188            | EntityKind::COMMAND_BLOCK_MINECART => [0.98, 0.7, 0.98],
189            EntityKind::PARROT => [0.5, 0.9, 0.5],
190            EntityKind::PHANTOM => [0.9, 0.5, 0.9],
191            EntityKind::PIGLIN_BRUTE => [0.6, 1.95, 0.6],
192            EntityKind::PILLAGER => [0.6, 1.95, 0.6],
193            EntityKind::TNT => [0.98, 0.98, 0.98],
194            EntityKind::PUFFERFISH => [0.7, 0.7, 0.7],
195            EntityKind::RAVAGER => [1.95, 2.2, 1.95],
196            EntityKind::SALMON => [0.7, 0.4, 0.7],
197            EntityKind::SHULKER_BULLET => [0.3125, 0.3125, 0.3125],
198            EntityKind::SILVERFISH => [0.4, 0.3, 0.4],
199            EntityKind::SMALL_FIREBALL => [0.3125, 0.3125, 0.3125],
200            EntityKind::SNOW_GOLEM => [0.7, 1.9, 0.7],
201            EntityKind::SPIDER => [1.4, 0.9, 1.4],
202            EntityKind::STRAY => [0.6, 1.99, 0.6],
203            EntityKind::EGG => [0.25, 0.25, 0.25],
204            EntityKind::ENDER_PEARL => [0.25, 0.25, 0.25],
205            EntityKind::EXPERIENCE_BOTTLE => [0.25, 0.25, 0.25],
206            EntityKind::SPLASH_POTION | EntityKind::LINGERING_POTION => [0.25, 0.25, 0.25],
207            EntityKind::TRIDENT => [0.5, 0.5, 0.5],
208            EntityKind::TRADER_LLAMA => [0.9, 1.87, 0.9],
209            EntityKind::TROPICAL_FISH => [0.5, 0.4, 0.5],
210            EntityKind::VEX => [0.4, 0.8, 0.4],
211            EntityKind::VINDICATOR => [0.6, 1.95, 0.6],
212            EntityKind::WITHER => [0.9, 3.5, 0.9],
213            EntityKind::WITHER_SKELETON => [0.7, 2.4, 0.7],
214            EntityKind::WITHER_SKULL => [0.3125, 0.3125, 0.3125],
215            EntityKind::FISHING_BOBBER => [0.25, 0.25, 0.25],
216            _ => {
217                continue;
218            }
219        }
220        .into();
221        hitbox.centered(size);
222    }
223}
224
225fn update_warden_hitbox(
226    mut query: Query<
227        (&mut HitboxShape, &entity::Pose),
228        (
229            Or<(Changed<entity::Pose>, Added<HitboxShape>)>,
230            With<warden::WardenEntity>,
231        ),
232    >,
233) {
234    for (mut hitbox, entity_pose) in &mut query {
235        hitbox.centered(
236            match entity_pose.0 {
237                Pose::Emerging | Pose::Digging => [0.9, 1.0, 0.9],
238                _ => [0.9, 2.9, 0.9],
239            }
240            .into(),
241        );
242    }
243}
244
245fn update_area_effect_cloud_hitbox(
246    mut query: Query<
247        (&mut HitboxShape, &area_effect_cloud::Radius),
248        Or<(Changed<area_effect_cloud::Radius>, Added<HitboxShape>)>,
249    >,
250) {
251    for (mut hitbox, cloud_radius) in &mut query {
252        let diameter = f64::from(cloud_radius.0) * 2.0;
253        hitbox.centered([diameter, 0.5, diameter].into());
254    }
255}
256
257fn update_armor_stand_hitbox(
258    mut query: Query<
259        (&mut HitboxShape, &armor_stand::ArmorStandFlags),
260        Or<(Changed<armor_stand::ArmorStandFlags>, Added<HitboxShape>)>,
261    >,
262) {
263    for (mut hitbox, stand_flags) in &mut query {
264        hitbox.centered(
265            if stand_flags.0 & 16 != 0 {
266                // Marker armor stand
267                [0.0; 3]
268            } else if stand_flags.0 & 1 != 0 {
269                // Small armor stand
270                [0.5, 0.9875, 0.5]
271            } else {
272                [0.5, 1.975, 0.5]
273            }
274            .into(),
275        );
276    }
277}
278
279fn child_hitbox(child: bool, v: DVec3) -> DVec3 {
280    if child {
281        v / 2.0
282    } else {
283        v
284    }
285}
286
287fn update_passive_child_hitbox(
288    mut query: Query<
289        (Entity, &mut HitboxShape, &EntityKind, &passive::Child),
290        Or<(Changed<passive::Child>, Added<HitboxShape>)>,
291    >,
292    pose_query: Query<&entity::Pose>,
293) {
294    for (entity, mut hitbox, entity_kind, child) in &mut query {
295        let big_s = match *entity_kind {
296            EntityKind::BEE => [0.7, 0.6, 0.7],
297            EntityKind::CAMEL => [1.7, 2.375, 1.7],
298            EntityKind::CHICKEN => [0.4, 0.7, 0.4],
299            EntityKind::DONKEY => [1.5, 1.39648, 1.5],
300            EntityKind::FOX => [0.6, 0.7, 0.6],
301            EntityKind::GOAT => {
302                if pose_query
303                    .get(entity)
304                    .is_ok_and(|v| v.0 == Pose::LongJumping)
305                {
306                    [0.63, 0.91, 0.63]
307                } else {
308                    [0.9, 1.3, 0.9]
309                }
310            }
311            EntityKind::HOGLIN => [1.39648, 1.4, 1.39648],
312            EntityKind::HORSE | EntityKind::SKELETON_HORSE | EntityKind::ZOMBIE_HORSE => {
313                [1.39648, 1.6, 1.39648]
314            }
315            EntityKind::LLAMA => [0.9, 1.87, 0.9],
316            EntityKind::MULE => [1.39648, 1.6, 1.39648],
317            EntityKind::MOOSHROOM => [0.9, 1.4, 0.9],
318            EntityKind::OCELOT => [0.6, 0.7, 0.6],
319            EntityKind::PANDA => [1.3, 1.25, 1.3],
320            EntityKind::PIG => [0.9, 0.9, 0.9],
321            EntityKind::POLAR_BEAR => [1.4, 1.4, 1.4],
322            EntityKind::RABBIT => [0.4, 0.5, 0.4],
323            EntityKind::SHEEP => [0.9, 1.3, 0.9],
324            EntityKind::TURTLE => {
325                hitbox.centered(
326                    if child.0 {
327                        [0.36, 0.12, 0.36]
328                    } else {
329                        [1.2, 0.4, 1.2]
330                    }
331                    .into(),
332                );
333                continue;
334            }
335            EntityKind::VILLAGER => [0.6, 1.95, 0.6],
336            EntityKind::WOLF => [0.6, 0.85, 0.6],
337            _ => {
338                continue;
339            }
340        };
341        hitbox.centered(child_hitbox(child.0, big_s.into()));
342    }
343}
344
345fn update_zombie_hitbox(
346    mut query: Query<
347        (&mut HitboxShape, &zombie::Baby),
348        Or<(Changed<zombie::Baby>, Added<HitboxShape>)>,
349    >,
350) {
351    for (mut hitbox, baby) in &mut query {
352        hitbox.centered(child_hitbox(baby.0, [0.6, 1.95, 0.6].into()));
353    }
354}
355
356fn update_piglin_hitbox(
357    mut query: Query<
358        (&mut HitboxShape, &piglin::Baby),
359        Or<(Changed<piglin::Baby>, Added<HitboxShape>)>,
360    >,
361) {
362    for (mut hitbox, baby) in &mut query {
363        hitbox.centered(child_hitbox(baby.0, [0.6, 1.95, 0.6].into()));
364    }
365}
366
367fn update_zoglin_hitbox(
368    mut query: Query<
369        (&mut HitboxShape, &zoglin::Baby),
370        Or<(Changed<zoglin::Baby>, Added<HitboxShape>)>,
371    >,
372) {
373    for (mut hitbox, baby) in &mut query {
374        hitbox.centered(child_hitbox(baby.0, [1.39648, 1.4, 1.39648].into()));
375    }
376}
377
378fn update_player_hitbox(
379    mut query: Query<
380        (&mut HitboxShape, &entity::Pose),
381        (
382            Or<(Changed<entity::Pose>, Added<HitboxShape>)>,
383            With<player::PlayerEntity>,
384        ),
385    >,
386) {
387    for (mut hitbox, pose) in &mut query {
388        hitbox.centered(
389            match pose.0 {
390                Pose::Sleeping | Pose::Dying => [0.2, 0.2, 0.2],
391                Pose::FallFlying | Pose::Swimming | Pose::SpinAttack => [0.6, 0.6, 0.6],
392                Pose::Sneaking => [0.6, 1.5, 0.6],
393                _ => [0.6, 1.8, 0.6],
394            }
395            .into(),
396        );
397    }
398}
399
400fn update_item_frame_hitbox(
401    mut query: Query<
402        (&mut HitboxShape, &item_frame::Rotation),
403        Or<(Changed<item_frame::Rotation>, Added<HitboxShape>)>,
404    >,
405) {
406    for (mut hitbox, rotation) in &mut query {
407        let mut center_pos = DVec3::splat(0.5);
408
409        const A: f64 = 0.46875;
410
411        match rotation.0 {
412            0 => center_pos.y += A,
413            1 => center_pos.y -= A,
414            2 => center_pos.z += A,
415            3 => center_pos.z -= A,
416            4 => center_pos.x += A,
417            5 => center_pos.x -= A,
418            _ => center_pos.y -= A,
419        }
420
421        const BOUNDS23: DVec3 = DVec3::new(0.375, 0.375, 0.03125);
422
423        let bounds = match rotation.0 {
424            2 | 3 => BOUNDS23,
425            4 | 5 => BOUNDS23.zxy(),
426            _ => BOUNDS23.zxy(),
427        };
428
429        hitbox.0 = Aabb::new(center_pos - bounds, center_pos + bounds);
430    }
431}
432
433fn update_slime_hitbox(
434    mut query: Query<
435        (&mut HitboxShape, &slime::SlimeSize),
436        Or<(Changed<slime::SlimeSize>, Added<HitboxShape>)>,
437    >,
438) {
439    for (mut hitbox, slime_size) in &mut query {
440        let s = 0.5202 * f64::from(slime_size.0);
441        hitbox.centered([s, s, s].into());
442    }
443}
444
445fn update_painting_hitbox(
446    mut query: Query<
447        (&mut HitboxShape, &painting::Variant, &Look),
448        Or<(
449            Changed<Look>,
450            Changed<painting::Variant>,
451            Added<HitboxShape>,
452        )>,
453    >,
454) {
455    for (mut hitbox, painting_variant, look) in &mut query {
456        let bounds = match &painting_variant.0 {
457            IdOr::Id(id) => PaintingKind::from_registry_id(*id)
458                .map_or_else(|| UVec3::splat(1), painting_kind_hitbox_bounds),
459            IdOr::Inline(inline) => inline_painting_hitbox_bounds(inline),
460        };
461
462        let mut center_pos = DVec3::splat(0.5);
463
464        let (facing_x, facing_z, cc_facing_x, cc_facing_z) =
465            match ((look.yaw + 45.0).rem_euclid(360.0) / 90.0) as u8 {
466                0 => (0, 1, 1, 0),   // South
467                1 => (-1, 0, 0, 1),  // West
468                2 => (0, -1, -1, 0), // North
469                _ => (1, 0, 0, -1),  // East
470            };
471
472        center_pos.x -= f64::from(facing_x) * 0.46875;
473        center_pos.z -= f64::from(facing_z) * 0.46875;
474
475        center_pos.x += f64::from(cc_facing_x) * if bounds.x.is_multiple_of(2) { 0.5 } else { 0.0 };
476        center_pos.y += if bounds.y.is_multiple_of(2) { 0.5 } else { 0.0 };
477        center_pos.z += f64::from(cc_facing_z) * if bounds.z.is_multiple_of(2) { 0.5 } else { 0.0 };
478
479        let bounds = match (facing_x, facing_z) {
480            (1 | -1, 0) => DVec3::new(0.0625, f64::from(bounds.y), f64::from(bounds.z)),
481            _ => DVec3::new(f64::from(bounds.x), f64::from(bounds.y), 0.0625),
482        };
483
484        hitbox.0 = Aabb::new(center_pos - bounds / 2.0, center_pos + bounds / 2.0);
485    }
486}
487
488fn painting_kind_hitbox_bounds(kind: PaintingKind) -> UVec3 {
489    match kind {
490        PaintingKind::Alban
491        | PaintingKind::Aztec
492        | PaintingKind::Aztec2
493        | PaintingKind::Bomb
494        | PaintingKind::Kebab
495        | PaintingKind::Meditative
496        | PaintingKind::Plant
497        | PaintingKind::Wasteland => [1, 1, 1],
498        PaintingKind::Graham | PaintingKind::PrairieRide | PaintingKind::Wanderer => [1, 2, 1],
499        PaintingKind::Courbet
500        | PaintingKind::Creebet
501        | PaintingKind::Pool
502        | PaintingKind::Sea
503        | PaintingKind::Sunset => [2, 1, 2],
504        PaintingKind::Baroque
505        | PaintingKind::Bust
506        | PaintingKind::Earth
507        | PaintingKind::Fire
508        | PaintingKind::Humble
509        | PaintingKind::Match
510        | PaintingKind::SkullAndRoses
511        | PaintingKind::Stage
512        | PaintingKind::Void
513        | PaintingKind::Water
514        | PaintingKind::Wind
515        | PaintingKind::Wither => [2, 2, 2],
516        PaintingKind::Bouquet
517        | PaintingKind::Cavebird
518        | PaintingKind::Cotan
519        | PaintingKind::Endboss
520        | PaintingKind::Fern
521        | PaintingKind::Owlemons
522        | PaintingKind::Sunflowers
523        | PaintingKind::Tides => [3, 3, 3],
524        PaintingKind::Backyard | PaintingKind::Pond => [3, 4, 3],
525        PaintingKind::Changing
526        | PaintingKind::Fighters
527        | PaintingKind::Finding
528        | PaintingKind::Lowmist
529        | PaintingKind::Passage => [4, 2, 4],
530        PaintingKind::DonkeyKong | PaintingKind::Skeleton => [4, 3, 4],
531        PaintingKind::BurningSkull
532        | PaintingKind::Orb
533        | PaintingKind::Pigscene
534        | PaintingKind::Pointer
535        | PaintingKind::Unpacked => [4, 4, 4],
536    }
537    .into()
538}
539
540fn inline_painting_hitbox_bounds(inline: &PaintingVariantDefinition) -> UVec3 {
541    let width = u32::try_from(inline.width)
542        .ok()
543        .filter(|&n| n > 0)
544        .unwrap_or(1);
545    let height = u32::try_from(inline.height)
546        .ok()
547        .filter(|&n| n > 0)
548        .unwrap_or(1);
549
550    UVec3::new(width, height, width)
551}
552
553fn update_shulker_hitbox(
554    mut query: Query<
555        (
556            &mut HitboxShape,
557            &shulker::PeekAmount,
558            &shulker::AttachedFace,
559        ),
560        Or<(
561            Changed<shulker::PeekAmount>,
562            Changed<shulker::AttachedFace>,
563            Added<HitboxShape>,
564        )>,
565    >,
566) {
567    use std::f64::consts::PI;
568
569    for (mut hitbox, peek_amount, attached_face) in &mut query {
570        let pos = DVec3::splat(0.5);
571        let mut min = pos - 0.5;
572        let mut max = pos + 0.5;
573
574        let peek = 0.5 - f64::cos(f64::from(peek_amount.0) * 0.01 * PI) * 0.5;
575
576        match attached_face.0 {
577            Direction::Down => max.y += peek,
578            Direction::Up => min.y -= peek,
579            Direction::North => max.z += peek,
580            Direction::South => min.z -= peek,
581            Direction::West => max.x += peek,
582            Direction::East => min.x -= peek,
583        }
584
585        hitbox.0 = Aabb::new(min, max);
586    }
587}