mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(block): Added more dispenser behaviors (#2495)
* feat(block): added DispenseContext and drop_item abstractions for dispenser behavior * feat(block): added specific arrow dispenser behavior * feat(block): added specific spawn dispenser behavior * docs(block): added some comments to quickly spot missing dispenser behaviors * fix: addressed PR review feedback
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use rand::{Rng, RngExt, rng};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::block::blocks::redstone::block_receives_redstone_power;
|
||||
use crate::block::registry::BlockActionResult;
|
||||
@@ -8,13 +9,26 @@ use crate::block::{
|
||||
BlockBehaviour, BlockFuture, GetComparatorOutputArgs, NormalUseArgs, OnNeighborUpdateArgs,
|
||||
OnPlaceArgs, OnScheduledTickArgs, PlacedArgs,
|
||||
};
|
||||
use crate::entity::decoration::armor_stand::ArmorStandEntity;
|
||||
use crate::entity::item::ItemEntity;
|
||||
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
||||
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::spawn_egg::apply_entity_variant;
|
||||
use crate::world::World;
|
||||
|
||||
use crate::block::entities::dispenser::DispenserBlockEntity;
|
||||
use pumpkin_data::BlockStateId;
|
||||
use pumpkin_data::block_properties::{BlockProperties, Facing};
|
||||
use pumpkin_data::entity::EntityType;
|
||||
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_inventory::generic_container_screen_handler::create_generic_3x3;
|
||||
@@ -23,7 +37,10 @@ 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;
|
||||
@@ -60,6 +77,22 @@ pub struct DispenserBlock;
|
||||
|
||||
type DispenserLikeProperties = pumpkin_data::block_properties::DispenserLikeProperties;
|
||||
|
||||
struct DispenseContext<'a> {
|
||||
world: &'a Arc<World>,
|
||||
position: &'a BlockPos,
|
||||
facing: Facing,
|
||||
}
|
||||
|
||||
impl<'a> DispenseContext<'a> {
|
||||
const fn new(args: &OnScheduledTickArgs<'a>, facing: Facing) -> Self {
|
||||
Self {
|
||||
world: args.world,
|
||||
position: args.position,
|
||||
facing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn triangle<R: Rng>(rng: &mut R, min: f64, max: f64) -> f64 {
|
||||
(rng.random::<f64>() - rng.random::<f64>()).mul_add(max, min)
|
||||
}
|
||||
@@ -163,42 +196,40 @@ impl BlockBehaviour for DispenserBlock {
|
||||
args.world.get_block_state(args.position).id,
|
||||
args.block,
|
||||
);
|
||||
let ctx = DispenseContext::new(&args, props.facing);
|
||||
|
||||
// No specific dispenser behaviors are registered yet, so dispense item into the world
|
||||
let drop_item = item.split(1);
|
||||
let facing = to_normal(props.facing);
|
||||
let mut position = args.position.to_centered_f64().add(&(facing * 0.7));
|
||||
// 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();
|
||||
|
||||
position.y -= match props.facing {
|
||||
Facing::Up | Facing::Down => 0.125,
|
||||
_ => 0.15625,
|
||||
};
|
||||
|
||||
let entity = Entity::new(args.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, drop_item, velocity, 40,
|
||||
));
|
||||
args.world.spawn_entity(item_entity).await;
|
||||
|
||||
args.world.sync_world_event(
|
||||
WorldEvent::SoundDispenserDispense,
|
||||
*args.position,
|
||||
0,
|
||||
);
|
||||
|
||||
args.world.sync_world_event(
|
||||
WorldEvent::ParticlesShootSmoke,
|
||||
*args.position,
|
||||
to_data3d(props.facing),
|
||||
);
|
||||
if arrows.contains(&item.item.id) {
|
||||
// Arrows
|
||||
Self::fire_arrow(&ctx, &mut item).await;
|
||||
} else if boats.contains(&item.item.id) {
|
||||
// Boats
|
||||
if !Self::dispense_boat(&ctx, &mut item).await {
|
||||
Self::drop_item(&ctx, &mut item).await;
|
||||
}
|
||||
} else if item.item.id == Item::ARMOR_STAND.id {
|
||||
// Armor stands
|
||||
if !Self::dispense_armor_stand(&ctx, &mut item).await {
|
||||
Self::drop_item(&ctx, &mut item).await;
|
||||
}
|
||||
} else if item.item.id == Item::TNT.id {
|
||||
// TNT
|
||||
Self::dispense_tnt(&ctx, &mut item).await;
|
||||
} else if entity_from_egg(item.item.id).is_some() {
|
||||
// Spawn eggs
|
||||
Self::dispense_spawn_egg(&ctx, &mut item).await;
|
||||
} else {
|
||||
// Default / Drop
|
||||
Self::drop_item(&ctx, &mut item).await;
|
||||
}
|
||||
} else {
|
||||
args.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserFail, *args.position, 0);
|
||||
@@ -222,3 +253,203 @@ impl BlockBehaviour for DispenserBlock {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DispenserBlock {
|
||||
const ARROW_DISPENSE_POWER: f64 = 1.1;
|
||||
const ARROW_DISPENSE_UNCERTAINTY: f64 = 6.0;
|
||||
|
||||
async fn fire_arrow(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
||||
// TODO: Add tipped arrows
|
||||
let entity_type = if item.item.id == Item::SPECTRAL_ARROW.id {
|
||||
&EntityType::SPECTRAL_ARROW
|
||||
} else {
|
||||
&EntityType::ARROW
|
||||
};
|
||||
let _ = item.split(1);
|
||||
|
||||
let facing = to_normal(ctx.facing);
|
||||
let position = ctx.position.to_centered_f64().add(&(facing * 0.7));
|
||||
let world = ctx.world;
|
||||
|
||||
let arrow_entity = Entity::new(world.clone(), position, entity_type);
|
||||
let mut arrow = ArrowEntity::new(arrow_entity, None);
|
||||
arrow.pickup = ArrowPickup::Allowed;
|
||||
|
||||
arrow.set_velocity(
|
||||
facing.x,
|
||||
facing.y + 0.1,
|
||||
facing.z,
|
||||
Self::ARROW_DISPENSE_POWER,
|
||||
Self::ARROW_DISPENSE_UNCERTAINTY,
|
||||
);
|
||||
|
||||
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
|
||||
world.spawn_entity(arrow_arc).await;
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserProjectileLaunch, *ctx.position, 0);
|
||||
|
||||
ctx.world.sync_world_event(
|
||||
WorldEvent::ParticlesShootSmoke,
|
||||
*ctx.position,
|
||||
to_data3d(ctx.facing),
|
||||
);
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
async 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)))
|
||||
.await;
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
||||
true
|
||||
}
|
||||
|
||||
async 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)))
|
||||
.await;
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
||||
true
|
||||
}
|
||||
|
||||
async 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).await;
|
||||
ctx.world
|
||||
.play_sound(Sound::EntityTntPrimed, SoundCategory::Blocks, &spawn_pos);
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
||||
}
|
||||
|
||||
async 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).await;
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
||||
}
|
||||
|
||||
async fn drop_item(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
|
||||
let drop_item = item.split(1);
|
||||
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, drop_item, velocity, 40,
|
||||
));
|
||||
ctx.world.spawn_entity(item_entity).await;
|
||||
|
||||
ctx.world
|
||||
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
|
||||
|
||||
ctx.world.sync_world_event(
|
||||
WorldEvent::ParticlesShootSmoke,
|
||||
*ctx.position,
|
||||
to_data3d(ctx.facing),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct BoatItem;
|
||||
|
||||
impl BoatItem {
|
||||
/// Maps boat item to corresponding entity type
|
||||
fn item_to_entity(item: &Item) -> &'static EntityType {
|
||||
pub(crate) fn item_to_entity(item: &Item) -> &'static EntityType {
|
||||
match item.id {
|
||||
val if val == Item::OAK_BOAT.id => &EntityType::OAK_BOAT,
|
||||
val if val == Item::OAK_CHEST_BOAT.id => &EntityType::OAK_CHEST_BOAT,
|
||||
|
||||
@@ -27,7 +27,7 @@ impl ItemMetadata for SpawnEggItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_entity_variant(item: &ItemStack, mob: &dyn EntityBase) {
|
||||
pub(crate) fn apply_entity_variant(item: &ItemStack, mob: &dyn EntityBase) {
|
||||
if let Some(comp) = item.get_data_component::<ChickenVariantImpl>() {
|
||||
mob.set_variant_name(&comp.value);
|
||||
} else if let Some(comp) = item.get_data_component::<FrogVariantImpl>() {
|
||||
|
||||
Reference in New Issue
Block a user