diff --git a/crates/pumpkin/src/block/blocks/mod.rs b/crates/pumpkin/src/block/blocks/mod.rs index a14241817..34e7a64fc 100644 --- a/crates/pumpkin/src/block/blocks/mod.rs +++ b/crates/pumpkin/src/block/blocks/mod.rs @@ -110,3 +110,5 @@ pub mod coral; pub mod abstract_wall_mounting; pub mod beacon; +pub mod trial_spawner; +pub mod vault; diff --git a/crates/pumpkin/src/block/blocks/trial_spawner.rs b/crates/pumpkin/src/block/blocks/trial_spawner.rs new file mode 100644 index 000000000..66ade631b --- /dev/null +++ b/crates/pumpkin/src/block/blocks/trial_spawner.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use pumpkin_data::block_properties::{ + BlockProperties, TrialSpawnerLikeProperties, TrialSpawnerState, +}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_macros::pumpkin_block; +use pumpkin_world::world::BlockFlags; + +use crate::block::entities::trial_spawner::TrialSpawnerBlockEntity; +use crate::block::registry::BlockActionResult; +use crate::block::{BlockBehaviour, BlockFuture, NormalUseArgs, PlacedArgs, UseWithItemArgs}; + +#[pumpkin_block("minecraft:trial_spawner")] +pub struct TrialSpawnerBlock; + +impl BlockBehaviour for TrialSpawnerBlock { + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let entity = TrialSpawnerBlockEntity::new(*args.position); + args.world.add_block_entity(Arc::new(entity)); + }) + } + + fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + let state_id = args.world.get_block_state_id(args.position); + let mut props = TrialSpawnerLikeProperties::from_state_id(state_id, args.block); + + match props.trial_spawner_state { + TrialSpawnerState::Inactive | TrialSpawnerState::WaitingForPlayers => { + props.trial_spawner_state = TrialSpawnerState::Active; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + args.world.play_sound( + Sound::BlockTrialSpawnerDetectPlayer, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.world.play_sound( + Sound::BlockTrialSpawnerOpenShutter, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + } + TrialSpawnerState::Active => { + // Eject trial rewards & key drop upon trial wave completion + props.trial_spawner_state = TrialSpawnerState::WaitingForRewardEjection; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + args.world.play_sound( + Sound::BlockTrialSpawnerSpawnItemBegin, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.world.play_sound( + Sound::BlockTrialSpawnerEjectItem, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + + let key_stack = ItemStack::new(1, &Item::TRIAL_KEY); + args.world.drop_stack(args.position, key_stack).await; + + props.trial_spawner_state = TrialSpawnerState::Cooldown; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + args.world.play_sound( + Sound::BlockTrialSpawnerCloseShutter, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + } + TrialSpawnerState::Cooldown => { + args.world.play_sound( + Sound::BlockTrialSpawnerAmbient, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + } + TrialSpawnerState::WaitingForRewardEjection | TrialSpawnerState::EjectingReward => { + props.trial_spawner_state = TrialSpawnerState::Cooldown; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + + BlockActionResult::Success + }) + } + + fn use_with_item<'a>( + &'a self, + args: UseWithItemArgs<'a>, + ) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + self.normal_use(NormalUseArgs { + server: args.server, + world: args.world, + block: args.block, + position: args.position, + player: args.player, + hit: args.hit, + }) + .await + }) + } +} diff --git a/crates/pumpkin/src/block/blocks/vault.rs b/crates/pumpkin/src/block/blocks/vault.rs new file mode 100644 index 000000000..293a600a8 --- /dev/null +++ b/crates/pumpkin/src/block/blocks/vault.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use pumpkin_data::BlockStateId; +use pumpkin_data::block_properties::{ + BlockProperties, LadderLikeProperties, VaultLikeProperties, VaultState, +}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_macros::pumpkin_block; +use pumpkin_world::world::BlockFlags; + +use crate::block::entities::vault::VaultBlockEntity; +use crate::block::registry::BlockActionResult; +use crate::block::{ + BlockBehaviour, BlockFuture, NormalUseArgs, OnPlaceArgs, PlacedArgs, UseWithItemArgs, +}; + +#[pumpkin_block("minecraft:vault")] +pub struct VaultBlock; + +impl BlockBehaviour for VaultBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let mut props = LadderLikeProperties::default(args.block); + props.facing = args + .player + .living_entity + .entity + .get_horizontal_facing() + .opposite(); + props.to_state_id(args.block) + }) + } + + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let entity = VaultBlockEntity::new(*args.position); + args.world.add_block_entity(Arc::new(entity)); + }) + } + + fn use_with_item<'a>( + &'a self, + args: UseWithItemArgs<'a>, + ) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + let item_id = args.item_stack.item.id; + let is_trial_key = + item_id == Item::TRIAL_KEY.id || item_id == Item::OMINOUS_TRIAL_KEY.id; + + if !is_trial_key { + args.world.play_sound( + Sound::BlockVaultInsertItemFail, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + return BlockActionResult::Success; + } + + if let Some(block_entity) = args.world.get_block_entity(args.position) + && let Some(vault_entity) = block_entity.as_any().downcast_ref::() + { + let player_uuid = args.player.gameprofile.id; + + if vault_entity.has_rewarded(&player_uuid).await { + args.world.play_sound( + Sound::BlockVaultRejectRewardedPlayer, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + return BlockActionResult::Success; + } + + vault_entity.mark_rewarded(player_uuid).await; + + args.item_stack + .decrement_unless_creative(args.player.gamemode.load(), 1); + + args.world.play_sound( + Sound::BlockVaultInsertItem, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.world.play_sound( + Sound::BlockVaultOpenShutter, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + + let state_id = args.world.get_block_state_id(args.position); + let mut props = VaultLikeProperties::from_state_id(state_id, args.block); + props.vault_state = VaultState::Ejecting; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + args.world.play_sound( + Sound::BlockVaultEjectItem, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + + // Spawn trial vault loot (emeralds, diamond, iron) + let loot_stacks = vec![ + ItemStack::new(4, &Item::EMERALD), + ItemStack::new(1, &Item::DIAMOND), + ItemStack::new(2, &Item::IRON_INGOT), + ]; + + for stack in loot_stacks { + args.world.drop_stack(args.position, stack).await; + } + + props.vault_state = VaultState::Active; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + return BlockActionResult::Success; + } + + BlockActionResult::Pass + }) + } + + fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + args.world.play_sound( + Sound::BlockVaultInsertItemFail, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + BlockActionResult::Success + }) + } +} diff --git a/crates/pumpkin/src/block/entities/vault.rs b/crates/pumpkin/src/block/entities/vault.rs index 2ee239843..233ebb325 100644 --- a/crates/pumpkin/src/block/entities/vault.rs +++ b/crates/pumpkin/src/block/entities/vault.rs @@ -1,13 +1,16 @@ use super::BlockEntity; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; +use std::collections::HashSet; use std::pin::Pin; use tokio::sync::Mutex; +use uuid::Uuid; pub struct VaultBlockEntity { pub position: BlockPos, pub config: Mutex>, pub server_data: Mutex>, + pub rewarded_players: Mutex>, } impl BlockEntity for VaultBlockEntity { @@ -27,6 +30,7 @@ impl BlockEntity for VaultBlockEntity { position, config: Mutex::new(nbt.get_compound("config").cloned()), server_data: Mutex::new(nbt.get_compound("server_data").cloned()), + rewarded_players: Mutex::new(HashSet::new()), } } @@ -66,12 +70,22 @@ impl BlockEntity for VaultBlockEntity { impl VaultBlockEntity { pub const ID: &'static str = "minecraft:vault"; + #[must_use] - pub const fn new(position: BlockPos) -> Self { + pub fn new(position: BlockPos) -> Self { Self { position, - config: Mutex::const_new(None), - server_data: Mutex::const_new(None), + config: Mutex::new(None), + server_data: Mutex::new(None), + rewarded_players: Mutex::new(HashSet::new()), } } + + pub async fn has_rewarded(&self, player_id: &Uuid) -> bool { + self.rewarded_players.lock().await.contains(player_id) + } + + pub async fn mark_rewarded(&self, player_id: Uuid) { + self.rewarded_players.lock().await.insert(player_id); + } } diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index 5aeb7a8ff..18ef89412 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -133,6 +133,8 @@ use crate::block::blocks::stairs::StairBlock; use crate::block::blocks::tnt::TNTBlock; use crate::block::blocks::torches::TorchBlock; use crate::block::blocks::trapdoor::TrapDoorBlock; +use crate::block::blocks::trial_spawner::TrialSpawnerBlock; +use crate::block::blocks::vault::VaultBlock; use crate::block::blocks::vine::VineBlock; use crate::block::blocks::walls::WallBlock; use crate::block::blocks::wither_skull::WitherSkeletonSkullBlock; @@ -248,6 +250,8 @@ pub fn default_registry() -> Arc { manager.register(SugarCaneBlock); manager.register(VineBlock); manager.register(TNTBlock); + manager.register(TrialSpawnerBlock); + manager.register(VaultBlock); manager.register(BushBlock); manager.register(FlowerBlock); manager.register(PotatoBlock);