Skip to main content

steel_core/behavior/items/
spawn_egg.rs

1//! Data-driven vanilla spawn-egg behavior.
2
3use std::sync::Arc;
4
5use steel_macros::item_behavior;
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_registry::data_components::components::EntityData;
8use steel_registry::data_components::vanilla_components::ENTITY_DATA;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::item_stack::ItemStack;
11use steel_registry::stat::vanilla_stat_types;
12use steel_registry::{vanilla_blocks, vanilla_game_events};
13use steel_utils::BlockPos;
14
15use crate::behavior::item_utils::get_player_pov_hit_result;
16use crate::behavior::{
17    BLOCK_BEHAVIORS, BlockCollisionContext, BlockStateBehaviorExt as _, ITEM_BEHAVIORS,
18    InteractionResult, InventoryAccess, ItemBehavior, UseItemContext, UseOnContext,
19};
20use crate::entity::{
21    AgeableMob, EntitySpawnPlacement, EntitySpawnReason, EntitySpawnRequest, Mob, SharedEntity,
22    add_spawned_entity, apply_implicit_item_stack_components, create_entity_instance, spawn_entity,
23};
24use crate::player::Player;
25use crate::world::ClipFluid;
26use crate::world::World;
27use crate::world::game_event::GameEventContext;
28
29/// Behavior for every data-driven `SpawnEggItem` registry entry.
30#[item_behavior(class = "SpawnEggItem")]
31pub struct SpawnEggItem;
32
33impl SpawnEggItem {
34    fn entity_type(stack: &ItemStack) -> Option<EntityTypeRef> {
35        stack.get(ENTITY_DATA).map(EntityData::entity_type)
36    }
37
38    fn spawn_mob(
39        world: &Arc<World>,
40        player: &Player,
41        inventory: &InventoryAccess,
42        stack: &ItemStack,
43        spawn_pos: BlockPos,
44        try_move_down: bool,
45        moved_up: bool,
46    ) -> InteractionResult {
47        let Some(entity_type) = Self::entity_type(stack) else {
48            return InteractionResult::Fail;
49        };
50
51        let request = EntitySpawnRequest {
52            entity_type,
53            placement: EntitySpawnPlacement::Block {
54                pos: spawn_pos,
55                try_move_down,
56                moved_up,
57            },
58            reason: EntitySpawnReason::SpawnItemUse,
59            finalize_spawn: true,
60            play_ambient_sound: true,
61            item_stack: Some(stack),
62            user_is_operator: player.is_operator(),
63        };
64        if spawn_entity(world, request).is_err() {
65            return InteractionResult::Fail;
66        }
67
68        inventory.with_item(|item| item.consume_one(player.has_infinite_materials()));
69        world.game_event(
70            &vanilla_game_events::ENTITY_PLACE,
71            spawn_pos,
72            &GameEventContext::new(Some(player), None),
73        );
74        InteractionResult::Success
75    }
76
77    /// Runs the interaction before normal mob handling.
78    pub(crate) fn interact_with_mob<M: Mob + ?Sized>(
79        stack: &mut ItemStack,
80        player: &Player,
81        parent: &M,
82    ) -> InteractionResult {
83        if ITEM_BEHAVIORS
84            .get_behavior(stack.item())
85            .as_spawn_egg()
86            .is_none()
87        {
88            return InteractionResult::Pass;
89        }
90        if Self::spawn_offspring(stack, parent).is_none() {
91            return InteractionResult::Pass;
92        }
93        stack.consume_one(player.has_infinite_materials());
94        InteractionResult::SuccessServer
95    }
96
97    fn spawn_offspring<M: Mob + ?Sized>(stack: &ItemStack, parent: &M) -> Option<SharedEntity> {
98        let entity_type = Self::entity_type(stack)?;
99        if entity_type != parent.entity_type() {
100            return None;
101        }
102
103        let world = parent.level()?;
104        let offspring = if let Some(ageable) = parent.as_ageable_mob() {
105            ageable.get_breed_offspring(&world, ageable)?
106        } else {
107            match create_entity_instance(&world, entity_type, parent.position()) {
108                Ok(offspring) => offspring,
109                Err(error) => {
110                    log::warn!(
111                        "Failed to create spawn-egg offspring {} at {:?}: {error:?}",
112                        entity_type.key,
113                        parent.position()
114                    );
115                    return None;
116                }
117            }
118        };
119
120        let ageable = offspring.as_ageable_mob()?;
121        ageable.set_baby(true);
122        if !AgeableMob::is_baby(ageable) {
123            return None;
124        }
125
126        offspring.base().set_position_local(parent.position());
127        offspring.set_rotation((0.0, 0.0));
128        offspring.set_old_position_to_current();
129        offspring.base().set_old_rotation_to_current();
130        apply_implicit_item_stack_components(&offspring, stack);
131        add_spawned_entity(&world, offspring.clone()).ok()?;
132        Some(offspring)
133    }
134}
135
136impl ItemBehavior for SpawnEggItem {
137    fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
138        Some(self)
139    }
140
141    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
142        let stack = context.inv.with_item(|item| item.clone());
143        if Self::entity_type(&stack).is_none() {
144            return InteractionResult::Fail;
145        }
146
147        let clicked_pos = context.hit_result.block_pos;
148        let clicked_state = context.world.get_block_state(clicked_pos);
149        if clicked_state.get_block() == &vanilla_blocks::SPAWNER {
150            // TODO: Use spawn eggs on a spawner block and also check if the condition is correct
151            // Spawner block-entity mutation is a separate foundation; do not
152            // incorrectly create a mob beside a spawner in its place.
153            return InteractionResult::Fail;
154        }
155
156        let clicked_face = context.hit_result.direction;
157        let shape = BLOCK_BEHAVIORS
158            .get_behavior(clicked_state.get_block())
159            .get_collision_shape(
160                clicked_state,
161                context.world.as_ref(),
162                clicked_pos,
163                BlockCollisionContext::empty(),
164            );
165        let spawn_pos = if shape.is_empty() {
166            clicked_pos
167        } else {
168            clicked_face.relative(clicked_pos)
169        };
170
171        let result = Self::spawn_mob(
172            context.world,
173            context.player,
174            &context.inv,
175            &stack,
176            spawn_pos,
177            true,
178            spawn_pos != clicked_pos && clicked_face == steel_utils::Direction::Up,
179        );
180        if result == InteractionResult::Success {
181            context
182                .player
183                .award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
184        }
185        result
186    }
187
188    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
189        let hit_result =
190            get_player_pov_hit_result(context.world, context.player, ClipFluid::SourceOnly);
191        if hit_result.miss {
192            return InteractionResult::Pass;
193        }
194
195        let stack = context.inv.with_item(|item| item.clone());
196        if Self::entity_type(&stack).is_none() {
197            return InteractionResult::Fail;
198        }
199
200        let pos = hit_result.block_pos;
201        let state = context.world.get_block_state(pos);
202        if !state.is_liquid_block() {
203            return InteractionResult::Pass;
204        }
205        if !context.world.may_interact(context.player, pos)
206            || !context
207                .player
208                .may_use_item_at(pos, hit_result.direction, &stack)
209        {
210            return InteractionResult::Fail;
211        }
212
213        let result = Self::spawn_mob(
214            context.world,
215            context.player,
216            &context.inv,
217            &stack,
218            pos,
219            false,
220            false,
221        );
222        if result == InteractionResult::Success {
223            context
224                .player
225                .award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
226        }
227        result
228    }
229}