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)]
24pub struct EntityHitboxSettings {
26 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#[derive(Component, Debug, PartialEq, Deref)]
75pub struct HitboxShape(pub Aabb);
76
77#[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 => [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 [0.0; 3]
268 } else if stand_flags.0 & 1 != 0 {
269 [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 { v / 2.0 } else { v }
281}
282
283fn update_passive_child_hitbox(
284 mut query: Query<
285 (Entity, &mut HitboxShape, &EntityKind, &passive::Child),
286 Or<(Changed<passive::Child>, Added<HitboxShape>)>,
287 >,
288 pose_query: Query<&entity::Pose>,
289) {
290 for (entity, mut hitbox, entity_kind, child) in &mut query {
291 let big_s = match *entity_kind {
292 EntityKind::BEE => [0.7, 0.6, 0.7],
293 EntityKind::CAMEL => [1.7, 2.375, 1.7],
294 EntityKind::CHICKEN => [0.4, 0.7, 0.4],
295 EntityKind::DONKEY => [1.5, 1.39648, 1.5],
296 EntityKind::FOX => [0.6, 0.7, 0.6],
297 EntityKind::GOAT => {
298 if pose_query
299 .get(entity)
300 .is_ok_and(|v| v.0 == Pose::LongJumping)
301 {
302 [0.63, 0.91, 0.63]
303 } else {
304 [0.9, 1.3, 0.9]
305 }
306 }
307 EntityKind::HOGLIN => [1.39648, 1.4, 1.39648],
308 EntityKind::HORSE | EntityKind::SKELETON_HORSE | EntityKind::ZOMBIE_HORSE => {
309 [1.39648, 1.6, 1.39648]
310 }
311 EntityKind::LLAMA => [0.9, 1.87, 0.9],
312 EntityKind::MULE => [1.39648, 1.6, 1.39648],
313 EntityKind::MOOSHROOM => [0.9, 1.4, 0.9],
314 EntityKind::OCELOT => [0.6, 0.7, 0.6],
315 EntityKind::PANDA => [1.3, 1.25, 1.3],
316 EntityKind::PIG => [0.9, 0.9, 0.9],
317 EntityKind::POLAR_BEAR => [1.4, 1.4, 1.4],
318 EntityKind::RABBIT => [0.4, 0.5, 0.4],
319 EntityKind::SHEEP => [0.9, 1.3, 0.9],
320 EntityKind::TURTLE => {
321 hitbox.centered(
322 if child.0 {
323 [0.36, 0.12, 0.36]
324 } else {
325 [1.2, 0.4, 1.2]
326 }
327 .into(),
328 );
329 continue;
330 }
331 EntityKind::VILLAGER => [0.6, 1.95, 0.6],
332 EntityKind::WOLF => [0.6, 0.85, 0.6],
333 _ => {
334 continue;
335 }
336 };
337 hitbox.centered(child_hitbox(child.0, big_s.into()));
338 }
339}
340
341fn update_zombie_hitbox(
342 mut query: Query<
343 (&mut HitboxShape, &zombie::Baby),
344 Or<(Changed<zombie::Baby>, Added<HitboxShape>)>,
345 >,
346) {
347 for (mut hitbox, baby) in &mut query {
348 hitbox.centered(child_hitbox(baby.0, [0.6, 1.95, 0.6].into()));
349 }
350}
351
352fn update_piglin_hitbox(
353 mut query: Query<
354 (&mut HitboxShape, &piglin::Baby),
355 Or<(Changed<piglin::Baby>, Added<HitboxShape>)>,
356 >,
357) {
358 for (mut hitbox, baby) in &mut query {
359 hitbox.centered(child_hitbox(baby.0, [0.6, 1.95, 0.6].into()));
360 }
361}
362
363fn update_zoglin_hitbox(
364 mut query: Query<
365 (&mut HitboxShape, &zoglin::Baby),
366 Or<(Changed<zoglin::Baby>, Added<HitboxShape>)>,
367 >,
368) {
369 for (mut hitbox, baby) in &mut query {
370 hitbox.centered(child_hitbox(baby.0, [1.39648, 1.4, 1.39648].into()));
371 }
372}
373
374fn update_player_hitbox(
375 mut query: Query<
376 (&mut HitboxShape, &entity::Pose),
377 (
378 Or<(Changed<entity::Pose>, Added<HitboxShape>)>,
379 With<player::PlayerEntity>,
380 ),
381 >,
382) {
383 for (mut hitbox, pose) in &mut query {
384 hitbox.centered(
385 match pose.0 {
386 Pose::Sleeping | Pose::Dying => [0.2, 0.2, 0.2],
387 Pose::FallFlying | Pose::Swimming | Pose::SpinAttack => [0.6, 0.6, 0.6],
388 Pose::Sneaking => [0.6, 1.5, 0.6],
389 _ => [0.6, 1.8, 0.6],
390 }
391 .into(),
392 );
393 }
394}
395
396fn update_item_frame_hitbox(
397 mut query: Query<
398 (&mut HitboxShape, &item_frame::Rotation),
399 Or<(Changed<item_frame::Rotation>, Added<HitboxShape>)>,
400 >,
401) {
402 for (mut hitbox, rotation) in &mut query {
403 let mut center_pos = DVec3::splat(0.5);
404
405 const A: f64 = 0.46875;
406
407 match rotation.0 {
408 0 => center_pos.y += A,
409 1 => center_pos.y -= A,
410 2 => center_pos.z += A,
411 3 => center_pos.z -= A,
412 4 => center_pos.x += A,
413 5 => center_pos.x -= A,
414 _ => center_pos.y -= A,
415 }
416
417 const BOUNDS23: DVec3 = DVec3::new(0.375, 0.375, 0.03125);
418
419 let bounds = match rotation.0 {
420 2 | 3 => BOUNDS23,
421 4 | 5 => BOUNDS23.zxy(),
422 _ => BOUNDS23.zxy(),
423 };
424
425 hitbox.0 = Aabb::new(center_pos - bounds, center_pos + bounds);
426 }
427}
428
429fn update_slime_hitbox(
430 mut query: Query<
431 (&mut HitboxShape, &slime::SlimeSize),
432 Or<(Changed<slime::SlimeSize>, Added<HitboxShape>)>,
433 >,
434) {
435 for (mut hitbox, slime_size) in &mut query {
436 let s = 0.5202 * f64::from(slime_size.0);
437 hitbox.centered([s, s, s].into());
438 }
439}
440
441fn update_painting_hitbox(
442 mut query: Query<
443 (&mut HitboxShape, &painting::Variant, &Look),
444 Or<(
445 Changed<Look>,
446 Changed<painting::Variant>,
447 Added<HitboxShape>,
448 )>,
449 >,
450) {
451 for (mut hitbox, painting_variant, look) in &mut query {
452 let bounds = match &painting_variant.0 {
453 IdOr::Id(id) => PaintingKind::from_registry_id(*id)
454 .map_or_else(|| UVec3::splat(1), painting_kind_hitbox_bounds),
455 IdOr::Inline(inline) => inline_painting_hitbox_bounds(inline),
456 };
457
458 let mut center_pos = DVec3::splat(0.5);
459
460 let (facing_x, facing_z, cc_facing_x, cc_facing_z) =
461 match ((look.yaw + 45.0).rem_euclid(360.0) / 90.0) as u8 {
462 0 => (0, 1, 1, 0), 1 => (-1, 0, 0, 1), 2 => (0, -1, -1, 0), _ => (1, 0, 0, -1), };
467
468 center_pos.x -= f64::from(facing_x) * 0.46875;
469 center_pos.z -= f64::from(facing_z) * 0.46875;
470
471 center_pos.x += f64::from(cc_facing_x) * if bounds.x.is_multiple_of(2) { 0.5 } else { 0.0 };
472 center_pos.y += if bounds.y.is_multiple_of(2) { 0.5 } else { 0.0 };
473 center_pos.z += f64::from(cc_facing_z) * if bounds.z.is_multiple_of(2) { 0.5 } else { 0.0 };
474
475 let bounds = match (facing_x, facing_z) {
476 (1 | -1, 0) => DVec3::new(0.0625, f64::from(bounds.y), f64::from(bounds.z)),
477 _ => DVec3::new(f64::from(bounds.x), f64::from(bounds.y), 0.0625),
478 };
479
480 hitbox.0 = Aabb::new(center_pos - bounds / 2.0, center_pos + bounds / 2.0);
481 }
482}
483
484fn painting_kind_hitbox_bounds(kind: PaintingKind) -> UVec3 {
485 match kind {
486 PaintingKind::Alban
487 | PaintingKind::Aztec
488 | PaintingKind::Aztec2
489 | PaintingKind::Bomb
490 | PaintingKind::Kebab
491 | PaintingKind::Meditative
492 | PaintingKind::Plant
493 | PaintingKind::Wasteland => [1, 1, 1],
494 PaintingKind::Graham | PaintingKind::PrairieRide | PaintingKind::Wanderer => [1, 2, 1],
495 PaintingKind::Courbet
496 | PaintingKind::Creebet
497 | PaintingKind::Pool
498 | PaintingKind::Sea
499 | PaintingKind::Sunset => [2, 1, 2],
500 PaintingKind::Baroque
501 | PaintingKind::Bust
502 | PaintingKind::Earth
503 | PaintingKind::Fire
504 | PaintingKind::Humble
505 | PaintingKind::Match
506 | PaintingKind::SkullAndRoses
507 | PaintingKind::Stage
508 | PaintingKind::Void
509 | PaintingKind::Water
510 | PaintingKind::Wind
511 | PaintingKind::Wither => [2, 2, 2],
512 PaintingKind::Bouquet
513 | PaintingKind::Cavebird
514 | PaintingKind::Cotan
515 | PaintingKind::Endboss
516 | PaintingKind::Fern
517 | PaintingKind::Owlemons
518 | PaintingKind::Sunflowers
519 | PaintingKind::Tides => [3, 3, 3],
520 PaintingKind::Backyard | PaintingKind::Pond => [3, 4, 3],
521 PaintingKind::Changing
522 | PaintingKind::Fighters
523 | PaintingKind::Finding
524 | PaintingKind::Lowmist
525 | PaintingKind::Passage => [4, 2, 4],
526 PaintingKind::DonkeyKong | PaintingKind::Skeleton => [4, 3, 4],
527 PaintingKind::BurningSkull
528 | PaintingKind::Orb
529 | PaintingKind::Pigscene
530 | PaintingKind::Pointer
531 | PaintingKind::Unpacked => [4, 4, 4],
532 }
533 .into()
534}
535
536fn inline_painting_hitbox_bounds(inline: &PaintingVariantDefinition) -> UVec3 {
537 let width = u32::try_from(inline.width)
538 .ok()
539 .filter(|&n| n > 0)
540 .unwrap_or(1);
541 let height = u32::try_from(inline.height)
542 .ok()
543 .filter(|&n| n > 0)
544 .unwrap_or(1);
545
546 UVec3::new(width, height, width)
547}
548
549fn update_shulker_hitbox(
550 mut query: Query<
551 (
552 &mut HitboxShape,
553 &shulker::PeekAmount,
554 &shulker::AttachedFace,
555 ),
556 Or<(
557 Changed<shulker::PeekAmount>,
558 Changed<shulker::AttachedFace>,
559 Added<HitboxShape>,
560 )>,
561 >,
562) {
563 use std::f64::consts::PI;
564
565 for (mut hitbox, peek_amount, attached_face) in &mut query {
566 let pos = DVec3::splat(0.5);
567 let mut min = pos - 0.5;
568 let mut max = pos + 0.5;
569
570 let peek = 0.5 - f64::cos(f64::from(peek_amount.0) * 0.01 * PI) * 0.5;
571
572 match attached_face.0 {
573 Direction::Down => max.y += peek,
574 Direction::Up => min.y -= peek,
575 Direction::North => max.z += peek,
576 Direction::South => min.z -= peek,
577 Direction::West => max.x += peek,
578 Direction::East => min.x -= peek,
579 }
580
581 hitbox.0 = Aabb::new(min, max);
582 }
583}