798 lines
28 KiB
Rust
798 lines
28 KiB
Rust
use rand::{Rng, RngExt, rng};
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use tokio::sync::Mutex;
|
|
use uuid::Uuid;
|
|
|
|
use crate::block::blocks::redstone::block_receives_redstone_power;
|
|
use crate::block::blocks::tnt::TNTBlock;
|
|
use crate::block::registry::BlockActionResult;
|
|
use crate::block::{
|
|
BlockBehaviour, GetComparatorOutputArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs,
|
|
OnScheduledTickArgs, PlacedArgs,
|
|
};
|
|
use crate::entity::decoration::armor_stand::ArmorStandEntity;
|
|
use crate::entity::item::ItemEntity;
|
|
use crate::entity::projectile::ThrownItemEntity;
|
|
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
|
use crate::entity::projectile::egg::EggEntity;
|
|
use crate::entity::projectile::firework_rocket::FireworkRocketEntity;
|
|
use crate::entity::projectile::lingering_potion::LingeringPotionEntity;
|
|
use crate::entity::projectile::small_fireball::SmallFireballEntity;
|
|
use crate::entity::projectile::snowball::SnowballEntity;
|
|
use crate::entity::projectile::splash_potion::SplashPotionEntity;
|
|
use crate::entity::projectile::wind_charge::{WIND_CHARGE_GRAVITY, WindChargeEntity};
|
|
use crate::entity::tnt::TNTEntity;
|
|
use crate::entity::r#type::from_type;
|
|
use crate::entity::vehicle::boat::BoatEntity;
|
|
use crate::entity::{Entity, EntityBase};
|
|
use crate::item::ItemMetadata;
|
|
use crate::item::items::boat::BoatItem;
|
|
use crate::item::items::bucket::{
|
|
FilledBucketItem, play_bucket_evaporation, should_evaporate_in_nether, try_pickup_fluid_at,
|
|
try_place_filled_bucket,
|
|
};
|
|
use crate::item::items::honeycomb::try_wax_block;
|
|
use crate::item::items::ignite::ignition::Ignition;
|
|
use crate::item::items::spawn_egg::apply_entity_variant;
|
|
use crate::world::World;
|
|
|
|
use crate::block::entities::dispenser::DispenserBlockEntity;
|
|
use pumpkin_data::block_properties::{BlockProperties, Facing};
|
|
use pumpkin_data::entity::{EntityType, entity_from_egg};
|
|
use pumpkin_data::fluid::Fluid;
|
|
use pumpkin_data::item::Item;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
|
use pumpkin_data::translation;
|
|
use pumpkin_data::world::WorldEvent;
|
|
use pumpkin_data::{Block, BlockStateId, FacingExt};
|
|
use pumpkin_inventory::generic_container_screen_handler::create_generic_3x3;
|
|
use pumpkin_inventory::player::player_inventory::PlayerInventory;
|
|
use pumpkin_inventory::screen_handler::{
|
|
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
|
|
};
|
|
use pumpkin_macros::pumpkin_block;
|
|
use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions};
|
|
use pumpkin_util::math::position::BlockPos;
|
|
use pumpkin_util::math::vector3::Vector3;
|
|
use pumpkin_util::math::wrap_degrees;
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_world::inventory::Inventory;
|
|
use pumpkin_world::tick::TickPriority;
|
|
use pumpkin_world::world::BlockFlags;
|
|
|
|
struct DispenserScreenFactory(Arc<dyn Inventory>);
|
|
|
|
impl ScreenHandlerFactory for DispenserScreenFactory {
|
|
fn create_screen_handler<'a>(
|
|
&'a self,
|
|
sync_id: u8,
|
|
player_inventory: &'a Arc<PlayerInventory>,
|
|
_player: &'a dyn InventoryPlayer,
|
|
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
|
Box::pin(async move {
|
|
let handler = create_generic_3x3(sync_id, player_inventory, self.0.clone()).await;
|
|
let screen_handler_arc = Arc::new(Mutex::new(handler));
|
|
|
|
Some(screen_handler_arc as SharedScreenHandler)
|
|
})
|
|
}
|
|
|
|
fn get_display_name(&self) -> TextComponent {
|
|
pumpkin_macros::translate_cross!(
|
|
translation::java::CONTAINER_DISPENSER,
|
|
translation::bedrock::CONTAINER_DISPENSER
|
|
)
|
|
}
|
|
}
|
|
|
|
#[pumpkin_block("minecraft:dispenser")]
|
|
pub struct DispenserBlock;
|
|
|
|
type DispenserLikeProperties = pumpkin_data::block_properties::DispenserLikeProperties;
|
|
|
|
struct DispenseContext<'a> {
|
|
world: &'a Arc<World>,
|
|
position: &'a BlockPos,
|
|
facing: Facing,
|
|
}
|
|
|
|
fn triangle<R: Rng>(rng: &mut R, min: f64, max: f64) -> f64 {
|
|
(rng.random::<f64>() - rng.random::<f64>()).mul_add(max, min)
|
|
}
|
|
|
|
const fn to_normal(facing: Facing) -> Vector3<f64> {
|
|
match facing {
|
|
Facing::North => Vector3::new(0., 0., -1.),
|
|
Facing::East => Vector3::new(1., 0., 0.),
|
|
Facing::South => Vector3::new(0., 0., 1.),
|
|
Facing::West => Vector3::new(-1., 0., 0.),
|
|
Facing::Up => Vector3::new(0., 1., 0.),
|
|
Facing::Down => Vector3::new(0., -1., 0.),
|
|
}
|
|
}
|
|
|
|
const fn to_data3d(facing: Facing) -> i32 {
|
|
match facing {
|
|
Facing::North => 2,
|
|
Facing::East => 5,
|
|
Facing::South => 3,
|
|
Facing::West => 4,
|
|
Facing::Up => 1,
|
|
Facing::Down => 0,
|
|
}
|
|
}
|
|
|
|
impl BlockBehaviour for DispenserBlock {
|
|
fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
|
|
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
|
&& let Some(inventory) = block_entity.get_inventory()
|
|
{
|
|
let player = Arc::clone(args.player);
|
|
let pos = *args.position;
|
|
tokio::spawn(async move {
|
|
player
|
|
.open_handled_screen(&DispenserScreenFactory(inventory), Some(pos))
|
|
.await;
|
|
});
|
|
}
|
|
BlockActionResult::Success
|
|
}
|
|
|
|
fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId {
|
|
let mut props = DispenserLikeProperties::default(args.block);
|
|
props.facing = args.player.get_entity().get_facing().opposite();
|
|
props.to_state_id(args.block)
|
|
}
|
|
|
|
fn placed(&self, args: PlacedArgs<'_>) {
|
|
let dispenser_block_entity = DispenserBlockEntity::new(*args.position);
|
|
args.world
|
|
.add_block_entity(Arc::new(dispenser_block_entity));
|
|
}
|
|
|
|
fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
|
|
let powered = block_receives_redstone_power(args.world, args.position)
|
|
|| block_receives_redstone_power(args.world, &args.position.up());
|
|
|
|
let mut props = DispenserLikeProperties::from_state_id(
|
|
args.world.get_block_state(args.position).id,
|
|
args.block,
|
|
);
|
|
|
|
if powered && !props.triggered {
|
|
args.world
|
|
.schedule_block_tick(args.block, *args.position, 4, TickPriority::Normal);
|
|
props.triggered = true;
|
|
args.world.set_block_state(
|
|
args.position,
|
|
props.to_state_id(args.block),
|
|
BlockFlags::NOTIFY_LISTENERS,
|
|
);
|
|
} else if !powered && props.triggered {
|
|
props.triggered = false;
|
|
args.world.set_block_state(
|
|
args.position,
|
|
props.to_state_id(args.block),
|
|
BlockFlags::NOTIFY_LISTENERS,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) {
|
|
let world = args.world.clone();
|
|
let position = *args.position;
|
|
tokio::spawn(async move {
|
|
let (block, state) = world.get_block_and_state(&position);
|
|
if let Some(block_entity) = world.get_block_entity(&position) {
|
|
let Some(dispenser) = block_entity.as_any().downcast_ref::<DispenserBlockEntity>()
|
|
else {
|
|
return;
|
|
};
|
|
|
|
if let Some((slot_index, mut item)) = dispenser.get_random_slot().await {
|
|
let props = DispenserLikeProperties::from_state_id(state.id, block);
|
|
let ctx = DispenseContext {
|
|
world: &world,
|
|
position: &position,
|
|
facing: props.facing,
|
|
};
|
|
Self::dispense(&ctx, dispenser, &mut item).await;
|
|
dispenser.set_stack(slot_index, item).await;
|
|
} else {
|
|
world.sync_world_event(WorldEvent::SoundDispenserFail, position, 0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option<u8> {
|
|
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
|
&& let Some(inventory) = block_entity.get_inventory()
|
|
{
|
|
Some(crate::block::calculate_comparator_output(
|
|
inventory.as_ref(),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DispenserBlock {
|
|
// Velocity values match the vanilla dispenser projectile settings.
|
|
const DEFAULT_PROJECTILE_POWER: f64 = 1.1;
|
|
const DEFAULT_PROJECTILE_UNCERTAINTY: f64 = 6.0;
|
|
const POTION_PROJECTILE_POWER: f64 = 1.375;
|
|
const POTION_PROJECTILE_UNCERTAINTY: f64 = 3.0;
|
|
// Fire charges and wind charges share these values.
|
|
const FIREBALL_PROJECTILE_POWER: f64 = 1.0;
|
|
const FIREBALL_PROJECTILE_UNCERTAINTY: f64 = 6.666_666_5;
|
|
const FIREWORK_PROJECTILE_POWER: f64 = 0.5;
|
|
const FIREWORK_PROJECTILE_UNCERTAINTY: f64 = 1.0;
|
|
|
|
async fn dispense(
|
|
ctx: &DispenseContext<'_>,
|
|
dispenser: &DispenserBlockEntity,
|
|
item: &mut ItemStack,
|
|
) {
|
|
let mut event = crate::plugin::api::events::block::block_dispense::BlockDispenseEvent::new(
|
|
*ctx.position,
|
|
item.item.registry_key.to_string(),
|
|
);
|
|
if let Some(server) = ctx.world.server.upgrade() {
|
|
server.plugin_manager.fire(&server, &mut event).await;
|
|
}
|
|
if event.cancelled {
|
|
ctx.world
|
|
.sync_world_event(WorldEvent::SoundDispenserFail, *ctx.position, 0);
|
|
return;
|
|
}
|
|
|
|
// Still missing some specific dispenser behavior that you can find here:
|
|
// https://minecraft.wiki/w/Dispenser#Usage
|
|
let arrows = [
|
|
Item::ARROW.id,
|
|
Item::TIPPED_ARROW.id,
|
|
Item::SPECTRAL_ARROW.id,
|
|
];
|
|
let boats = BoatItem::ids();
|
|
|
|
if arrows.contains(&item.item.id) {
|
|
// Arrows
|
|
Self::fire_arrow(ctx, item);
|
|
} else if boats.contains(&item.item.id) {
|
|
// Boats
|
|
if !Self::dispense_boat(ctx, item) {
|
|
Self::drop_item(ctx, item);
|
|
}
|
|
} else if item.item.id == Item::ARMOR_STAND.id {
|
|
// Armor stands
|
|
if !Self::dispense_armor_stand(ctx, item) {
|
|
Self::drop_item(ctx, item);
|
|
}
|
|
} else if item.item.id == Item::TNT.id {
|
|
// TNT
|
|
Self::dispense_tnt(ctx, item);
|
|
} else if item.item.id == Item::SNOWBALL.id {
|
|
Self::dispense_snowball(ctx, item);
|
|
} else if item.item.id == Item::EGG.id {
|
|
Self::dispense_egg(ctx, item);
|
|
} else if item.item.id == Item::SPLASH_POTION.id {
|
|
Self::dispense_splash_potion(ctx, item);
|
|
} else if item.item.id == Item::LINGERING_POTION.id {
|
|
Self::dispense_lingering_potion(ctx, item);
|
|
} else if item.item.id == Item::FIRE_CHARGE.id {
|
|
Self::dispense_fire_charge(ctx, item);
|
|
} else if item.item.id == Item::WIND_CHARGE.id {
|
|
Self::dispense_wind_charge(ctx, item);
|
|
} else if item.item.id == Item::FIREWORK_ROCKET.id {
|
|
Self::dispense_firework_rocket(ctx, item);
|
|
} else if item.item.id == Item::BUCKET.id {
|
|
// Empty buckets pick up the fluid in front of the dispenser
|
|
Self::dispense_empty_bucket(ctx, dispenser, item).await;
|
|
} else if FilledBucketItem::ids().contains(&item.item.id) {
|
|
// Filled buckets place their fluid in front of the dispenser
|
|
Self::dispense_filled_bucket(ctx, item).await;
|
|
} else if item.item.id == Item::FLINT_AND_STEEL.id {
|
|
// Flint and steel light fires and prime TNT
|
|
Self::dispense_flint_and_steel(ctx, item).await;
|
|
} else if item.item.id == Item::HONEYCOMB.id {
|
|
// Honeycombs wax copper blocks
|
|
Self::dispense_honeycomb(ctx, item).await;
|
|
} else if entity_from_egg(item.item.id).is_some() {
|
|
// Spawn eggs
|
|
Self::dispense_spawn_egg(ctx, item);
|
|
} else {
|
|
// Default / Drop
|
|
Self::drop_item(ctx, item);
|
|
}
|
|
}
|
|
|
|
fn projectile_spawn_position(ctx: &DispenseContext<'_>) -> Vector3<f64> {
|
|
ctx.position
|
|
.to_centered_f64()
|
|
.add(&(to_normal(ctx.facing) * 0.7))
|
|
}
|
|
|
|
fn launch_thrown(
|
|
ctx: &DispenseContext<'_>,
|
|
thrown: &ThrownItemEntity,
|
|
power: f64,
|
|
uncertainty: f64,
|
|
) {
|
|
let facing = to_normal(ctx.facing);
|
|
thrown.set_velocity(facing.x, facing.y + 0.1, facing.z, power, uncertainty);
|
|
}
|
|
|
|
fn finish_projectile_launch(
|
|
ctx: &DispenseContext<'_>,
|
|
projectile: Arc<dyn EntityBase>,
|
|
launch_event: WorldEvent,
|
|
) {
|
|
ctx.world.spawn_entity(projectile);
|
|
Self::play_dispense_effects(ctx, launch_event);
|
|
}
|
|
|
|
fn play_dispense_effects(ctx: &DispenseContext<'_>, sound_event: WorldEvent) {
|
|
ctx.world.sync_world_event(sound_event, *ctx.position, 0);
|
|
ctx.world.sync_world_event(
|
|
WorldEvent::ParticlesShootSmoke,
|
|
*ctx.position,
|
|
to_data3d(ctx.facing),
|
|
);
|
|
}
|
|
|
|
fn fire_arrow(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let projectile = item.split(1);
|
|
|
|
let facing = to_normal(ctx.facing);
|
|
let arrow_entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
ArrowEntity::entity_type_for_item(projectile.item),
|
|
);
|
|
let arrow =
|
|
ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed);
|
|
|
|
arrow.set_velocity(
|
|
facing.x,
|
|
facing.y + 0.1,
|
|
facing.z,
|
|
Self::DEFAULT_PROJECTILE_POWER,
|
|
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(arrow),
|
|
WorldEvent::SoundDispenserProjectileLaunch,
|
|
);
|
|
}
|
|
|
|
fn target_position(ctx: &DispenseContext<'_>) -> BlockPos {
|
|
let facing = to_normal(ctx.facing);
|
|
ctx.position.offset(Vector3::new(
|
|
facing.x as i32,
|
|
facing.y as i32,
|
|
facing.z as i32,
|
|
))
|
|
}
|
|
|
|
fn has_room_for(
|
|
ctx: &DispenseContext<'_>,
|
|
spawn_pos: Vector3<f64>,
|
|
size: &EntityDimensions,
|
|
) -> bool {
|
|
let bounding_box = BoundingBox::new_from_pos(spawn_pos.x, spawn_pos.y, spawn_pos.z, size);
|
|
ctx.world.is_space_empty(bounding_box)
|
|
&& ctx.world.get_entities_at_box(&bounding_box).is_empty()
|
|
}
|
|
|
|
fn dispense_boat(ctx: &DispenseContext<'_>, item: &mut ItemStack) -> bool {
|
|
let target = Self::target_position(ctx);
|
|
let is_water = |id: u16| id == Fluid::WATER.id || id == Fluid::FLOWING_WATER.id;
|
|
|
|
let spawn_pos = if is_water(ctx.world.get_fluid(&target).id) {
|
|
target.to_f64()
|
|
} else if ctx.world.get_block_state(&target).is_air()
|
|
&& is_water(ctx.world.get_fluid(&target.down()).id)
|
|
{
|
|
target.down().to_f64()
|
|
} else {
|
|
return false;
|
|
};
|
|
|
|
let entity_type = BoatItem::item_to_entity(item.item);
|
|
let dimensions = EntityDimensions::new(
|
|
entity_type.dimension[0],
|
|
entity_type.dimension[1],
|
|
entity_type.eye_height,
|
|
);
|
|
if !Self::has_room_for(ctx, spawn_pos, &dimensions) {
|
|
return false;
|
|
}
|
|
|
|
let _ = item.split(1);
|
|
let facing = to_normal(ctx.facing);
|
|
let entity = Entity::new(ctx.world.clone(), spawn_pos, entity_type);
|
|
entity.set_rotation(facing.x.atan2(facing.z) as f32 * 57.295_776, 0.0);
|
|
ctx.world.spawn_entity(Arc::new(BoatEntity::new(entity)));
|
|
|
|
ctx.world
|
|
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
|
true
|
|
}
|
|
|
|
fn dispense_armor_stand(ctx: &DispenseContext<'_>, item: &mut ItemStack) -> bool {
|
|
let target = Self::target_position(ctx);
|
|
let spawn_pos = target.to_f64();
|
|
let dimensions = EntityDimensions::new(
|
|
EntityType::ARMOR_STAND.dimension[0],
|
|
EntityType::ARMOR_STAND.dimension[1],
|
|
EntityType::ARMOR_STAND.eye_height,
|
|
);
|
|
if !Self::has_room_for(ctx, spawn_pos, &dimensions) {
|
|
return false;
|
|
}
|
|
|
|
let _ = item.split(1);
|
|
let facing = to_normal(ctx.facing);
|
|
let entity = Entity::new(ctx.world.clone(), spawn_pos, &EntityType::ARMOR_STAND);
|
|
entity.set_rotation(facing.x.atan2(facing.z) as f32 * 57.295_776, 0.0);
|
|
|
|
ctx.world.play_sound(
|
|
Sound::EntityArmorStandPlace,
|
|
SoundCategory::Blocks,
|
|
&spawn_pos,
|
|
);
|
|
ctx.world
|
|
.spawn_entity(Arc::new(ArmorStandEntity::new(entity)));
|
|
|
|
ctx.world
|
|
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
|
true
|
|
}
|
|
|
|
fn dispense_tnt(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
const TNT_POWER: f32 = 4.0;
|
|
const TNT_FUSE: u32 = 80;
|
|
|
|
let _ = item.split(1);
|
|
let spawn_pos = Self::target_position(ctx).to_f64();
|
|
|
|
let entity = Entity::new(ctx.world.clone(), spawn_pos, &EntityType::TNT);
|
|
let tnt = Arc::new(TNTEntity::new(entity, TNT_POWER, TNT_FUSE));
|
|
ctx.world.spawn_entity(tnt);
|
|
ctx.world
|
|
.play_sound(Sound::EntityTntPrimed, SoundCategory::Blocks, &spawn_pos);
|
|
|
|
ctx.world
|
|
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
|
}
|
|
|
|
fn dispense_spawn_egg(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let Some(entity_type) = entity_from_egg(item.item.id) else {
|
|
return;
|
|
};
|
|
|
|
let _ = item.split(1);
|
|
let spawn_pos = Self::target_position(ctx).to_f64();
|
|
|
|
let mob = from_type(entity_type, spawn_pos, ctx.world, Uuid::new_v4());
|
|
let yaw = wrap_degrees(rng().random::<f32>() * 360.0) % 360.0;
|
|
mob.get_entity().set_rotation(yaw, 0.0);
|
|
apply_entity_variant(item, mob.as_ref());
|
|
|
|
ctx.world.spawn_entity(mob);
|
|
|
|
ctx.world
|
|
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
|
}
|
|
|
|
fn dispense_snowball(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let _ = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::SNOWBALL,
|
|
);
|
|
let snowball = SnowballEntity::new(entity);
|
|
Self::launch_thrown(
|
|
ctx,
|
|
&snowball.thrown,
|
|
Self::DEFAULT_PROJECTILE_POWER,
|
|
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(snowball),
|
|
WorldEvent::SoundDispenserProjectileLaunch,
|
|
);
|
|
}
|
|
|
|
fn dispense_egg(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let projectile = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::EGG,
|
|
);
|
|
let egg = EggEntity::new(entity);
|
|
egg.set_item_stack(projectile);
|
|
Self::launch_thrown(
|
|
ctx,
|
|
&egg.thrown,
|
|
Self::DEFAULT_PROJECTILE_POWER,
|
|
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(egg),
|
|
WorldEvent::SoundDispenserProjectileLaunch,
|
|
);
|
|
}
|
|
|
|
fn dispense_splash_potion(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let projectile = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::SPLASH_POTION,
|
|
);
|
|
let potion = SplashPotionEntity::new(entity);
|
|
potion.set_item_stack(projectile);
|
|
Self::launch_thrown(
|
|
ctx,
|
|
&potion.thrown,
|
|
Self::POTION_PROJECTILE_POWER,
|
|
Self::POTION_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(potion),
|
|
WorldEvent::SoundDispenserProjectileLaunch,
|
|
);
|
|
}
|
|
|
|
fn dispense_lingering_potion(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let projectile = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::LINGERING_POTION,
|
|
);
|
|
let potion = LingeringPotionEntity::new(entity);
|
|
potion.set_item_stack(projectile);
|
|
Self::launch_thrown(
|
|
ctx,
|
|
&potion.thrown,
|
|
Self::POTION_PROJECTILE_POWER,
|
|
Self::POTION_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(potion),
|
|
WorldEvent::SoundDispenserProjectileLaunch,
|
|
);
|
|
}
|
|
|
|
fn dispense_fire_charge(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let _ = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::SMALL_FIREBALL,
|
|
);
|
|
let fireball = SmallFireballEntity::new(entity);
|
|
// Vanilla aims fire charges straight along the facing axis, without the +0.1 Y bias
|
|
// other projectiles get.
|
|
let facing = to_normal(ctx.facing);
|
|
fireball.thrown.set_velocity(
|
|
facing.x,
|
|
facing.y,
|
|
facing.z,
|
|
Self::FIREBALL_PROJECTILE_POWER,
|
|
Self::FIREBALL_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(ctx, Arc::new(fireball), WorldEvent::SoundBlazeFireball);
|
|
}
|
|
|
|
fn dispense_wind_charge(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let _ = item.split(1);
|
|
let entity = Entity::new(
|
|
ctx.world.clone(),
|
|
Self::projectile_spawn_position(ctx),
|
|
&EntityType::WIND_CHARGE,
|
|
);
|
|
let thrown = ThrownItemEntity {
|
|
entity,
|
|
owner_id: None,
|
|
collides_with_projectiles: false,
|
|
has_hit: AtomicBool::new(false),
|
|
gravity: WIND_CHARGE_GRAVITY,
|
|
};
|
|
Self::launch_thrown(
|
|
ctx,
|
|
&thrown,
|
|
Self::FIREBALL_PROJECTILE_POWER,
|
|
Self::FIREBALL_PROJECTILE_UNCERTAINTY,
|
|
);
|
|
Self::finish_projectile_launch(
|
|
ctx,
|
|
Arc::new(WindChargeEntity::new_normal(thrown)),
|
|
WorldEvent::SoundWindChargeShoot,
|
|
);
|
|
}
|
|
|
|
fn dispense_firework_rocket(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let _ = item.split(1);
|
|
let facing = to_normal(ctx.facing);
|
|
// Vanilla spawns fireworks closer to the dispenser face and slightly above center.
|
|
let position = ctx
|
|
.position
|
|
.to_centered_f64()
|
|
.add(&(facing * (0.7 * 0.5125)))
|
|
.add(&Vector3::new(0.0, 0.08, 0.0));
|
|
let entity = Entity::new(ctx.world.clone(), position, &EntityType::FIREWORK_ROCKET);
|
|
let rocket = FireworkRocketEntity::new(entity);
|
|
|
|
// `FireworkRocketEntity` does not expose its inner projectile, so replicate
|
|
// `ThrownItemEntity::set_velocity` here.
|
|
let deviation = 0.017_227_5 * Self::FIREWORK_PROJECTILE_UNCERTAINTY;
|
|
let velocity = Vector3::new(facing.x, facing.y + 0.1, facing.z)
|
|
.normalize()
|
|
.add_raw(
|
|
triangle(&mut rng(), 0.0, deviation),
|
|
triangle(&mut rng(), 0.0, deviation),
|
|
triangle(&mut rng(), 0.0, deviation),
|
|
)
|
|
.multiply(
|
|
Self::FIREWORK_PROJECTILE_POWER,
|
|
Self::FIREWORK_PROJECTILE_POWER,
|
|
Self::FIREWORK_PROJECTILE_POWER,
|
|
);
|
|
let rocket_entity = rocket.get_entity();
|
|
rocket_entity.set_velocity(velocity);
|
|
rocket_entity.set_rotation(
|
|
velocity.x.atan2(velocity.z) as f32 * 57.295_776,
|
|
velocity.y.atan2(velocity.horizontal_length()) as f32 * 57.295_776,
|
|
);
|
|
|
|
Self::finish_projectile_launch(ctx, Arc::new(rocket), WorldEvent::SoundFireworkShoot);
|
|
}
|
|
|
|
async fn dispense_empty_bucket(
|
|
ctx: &DispenseContext<'_>,
|
|
dispenser: &DispenserBlockEntity,
|
|
item: &mut ItemStack,
|
|
) {
|
|
let front = Self::target_position(ctx);
|
|
let Some(filled) = try_pickup_fluid_at(ctx.world, front) else {
|
|
Self::drop_item(ctx, item);
|
|
return;
|
|
};
|
|
|
|
item.decrement(1);
|
|
let filled_stack = ItemStack::new(1, filled);
|
|
if item.is_empty() {
|
|
*item = filled_stack;
|
|
} else if let Some(rest) = Self::add_to_first_free_slot(dispenser, filled_stack).await {
|
|
Self::eject_item(ctx, rest);
|
|
}
|
|
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
|
|
}
|
|
|
|
/// Places `stack` into the first empty slot, returning it back if every slot is occupied.
|
|
/// The slot currently being dispensed from still holds its pre-dispense stack, so it is
|
|
/// never considered free.
|
|
async fn add_to_first_free_slot(
|
|
dispenser: &DispenserBlockEntity,
|
|
stack: ItemStack,
|
|
) -> Option<ItemStack> {
|
|
let mut items = dispenser.items.write().await;
|
|
for slot in items.iter_mut() {
|
|
if slot.is_empty() {
|
|
*slot = stack;
|
|
dispenser.mark_dirty();
|
|
return None;
|
|
}
|
|
}
|
|
Some(stack)
|
|
}
|
|
|
|
async fn dispense_filled_bucket(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let front = Self::target_position(ctx);
|
|
|
|
// TODO: Spawn the stored entity for axolotl/fish/tadpole buckets, like the player path.
|
|
let emptied = if should_evaporate_in_nether(item.item, ctx.world) {
|
|
play_bucket_evaporation(ctx.world, &front.to_f64());
|
|
true
|
|
} else {
|
|
try_place_filled_bucket(
|
|
ctx.world,
|
|
item.item,
|
|
*ctx.position,
|
|
ctx.facing.to_block_direction(),
|
|
)
|
|
};
|
|
|
|
if emptied {
|
|
*item = ItemStack::new(1, &Item::BUCKET);
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
|
|
} else {
|
|
Self::drop_item(ctx, item);
|
|
}
|
|
}
|
|
|
|
async fn dispense_flint_and_steel(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let front = Self::target_position(ctx);
|
|
let front_block = ctx.world.get_block(&front);
|
|
|
|
let ignited = if front_block == &Block::TNT {
|
|
TNTBlock::prime(ctx.world, &front);
|
|
true
|
|
} else {
|
|
Ignition::ignite_block(
|
|
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {
|
|
world.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL);
|
|
},
|
|
ctx.world,
|
|
front,
|
|
front,
|
|
front_block,
|
|
)
|
|
};
|
|
|
|
if ignited {
|
|
// `damage_item` already consumes the tool from the stack when it breaks.
|
|
let _ = item.damage_item(1);
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
|
|
} else {
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserFail);
|
|
}
|
|
}
|
|
|
|
async fn dispense_honeycomb(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let front = Self::target_position(ctx);
|
|
let front_block = ctx.world.get_block(&front);
|
|
|
|
if try_wax_block(ctx.world, front, front_block) {
|
|
item.decrement(1);
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
|
|
} else {
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserFail);
|
|
}
|
|
}
|
|
|
|
fn drop_item(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
|
let drop_item = item.split(1);
|
|
Self::eject_item(ctx, drop_item);
|
|
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
|
|
}
|
|
|
|
fn eject_item(ctx: &DispenseContext<'_>, stack: ItemStack) {
|
|
let facing = to_normal(ctx.facing);
|
|
let mut position = ctx.position.to_centered_f64().add(&(facing * 0.7));
|
|
|
|
position.y -= match ctx.facing {
|
|
Facing::Up | Facing::Down => 0.125,
|
|
_ => 0.15625,
|
|
};
|
|
|
|
let entity = Entity::new(ctx.world.clone(), position, &EntityType::ITEM);
|
|
let rd = rng().random::<f64>().mul_add(0.1, 0.2);
|
|
|
|
let velocity = Vector3::new(
|
|
triangle(&mut rng(), facing.x * rd, 0.017_227_5 * 6.),
|
|
triangle(&mut rng(), 0.2, 0.017_227_5 * 6.),
|
|
triangle(&mut rng(), facing.z * rd, 0.017_227_5 * 6.),
|
|
);
|
|
|
|
let item_entity = Arc::new(ItemEntity::new_with_velocity(entity, stack, velocity, 40));
|
|
ctx.world.spawn_entity(item_entity);
|
|
}
|
|
}
|