steel_core/entity/pushing/
mod.rs1use std::cell::RefCell;
4
5use steel_registry::vanilla_game_rules::MAX_ENTITY_CRAMMING;
6
7use super::selector::EntitySelector;
8use super::{Entity, LivingEntity, SharedEntity};
9use crate::world::World;
10
11pub use snapshot::{DefaultEntityPush, EntityPushSnapshot};
12
13mod impulse;
14mod snapshot;
15
16thread_local! {
17 static SELECTED_PUSH_STATES: RefCell<Vec<Option<EntityPushSnapshot>>> = const { RefCell::new(Vec::new()) };
18}
19
20pub(crate) struct EntityPush;
21
22impl EntityPush {
23 pub(crate) fn push_entities<E: LivingEntity + ?Sized>(pusher: &E) {
24 let Some(world) = pusher.level() else {
25 return;
26 };
27
28 if !world.tick_runs_normally() {
29 return;
30 }
31
32 let target = pusher.as_entity_event_source();
33 let candidates = if let Some(default_push) = target.as_default_entity_push() {
34 let max_cramming = world.get_game_rule(&MAX_ENTITY_CRAMMING);
35 let target_id = target.id();
36 let borrow_limit = usize::try_from(max_cramming)
37 .ok()
38 .filter(|&limit| limit > 0);
39 let fallback = world.entity_manager().try_with_entities_in_aabb(
40 &target.bounding_box(),
41 borrow_limit,
42 |entities| {
43 let below_cramming_limit = max_cramming <= 0
44 || entities.len() < max_cramming as usize
45 || (entities.len() == max_cramming as usize
46 && entities.iter().any(|entity| entity.id() == target_id));
47
48 below_cramming_limit
49 && entities
50 .iter()
51 .all(|entity| entity.as_default_entity_push().is_some())
52 && Self::push_selected_entities(default_push, entities)
53 },
54 );
55
56 let Some(candidates) = fallback else {
57 return;
58 };
59
60 candidates
61 } else {
62 world.get_entities_in_aabb(&target.bounding_box())
63 };
64
65 let entities = World::filter_pushable_entities(target, candidates);
66
67 if entities.is_empty() {
68 return;
69 }
70
71 pusher.apply_entity_cramming_damage(&world, &entities);
72 pusher.push_entities_in_order(&entities);
73 }
74
75 fn push_selected_entities(target: &dyn DefaultEntityPush, entities: &[&SharedEntity]) -> bool {
76 SELECTED_PUSH_STATES.with(|states| {
77 let mut states = states.borrow_mut();
78 states.clear();
79 let predicate = EntitySelector::pushable_by(target);
80
81 for entity in entities {
82 let Some(source) = entity.as_default_entity_push() else {
83 return false;
84 };
85
86 if !predicate(entity.as_ref()) {
87 states.push(None);
88 continue;
89 }
90
91 let Some(state) = source.push_snapshot(true) else {
92 return false;
93 };
94
95 states.push(Some(state));
96 }
97
98 if states.iter().all(Option::is_none) {
99 return true;
100 }
101
102 let Some(target_state) = target.push_snapshot(false) else {
103 return false;
104 };
105
106 for (entity, source) in entities.iter().zip(states.iter()) {
109 if let Some(source) = source {
110 Self::apply_contact(entity.as_ref(), *source, target, target_state);
111 }
112 }
113
114 true
115 })
116 }
117
118 pub(crate) fn push_entities_in_order(target: &dyn Entity, entities: &[SharedEntity]) {
119 let Some(default_target) = target.as_default_entity_push() else {
120 for entity in entities {
121 entity.push_entity(target);
122 }
123
124 return;
125 };
126
127 let mut cached = None;
128
129 for entity in entities {
130 if let Some(source) = entity.as_default_entity_push()
131 && let Some(target_state) =
132 *cached.get_or_insert_with(|| default_target.push_snapshot(false))
133 && let Some(source_state) = source.push_snapshot(false)
134 {
135 if entity.id() != target.id() {
136 Self::apply_contact(entity.as_ref(), source_state, target, target_state);
137 }
138
139 continue;
140 }
141
142 cached = None;
143 entity.push_entity(target);
144 }
145 }
146}
147
148#[cfg(test)]
149mod test_fixtures;
150#[cfg(test)]
151mod tests;