feat(block): implement vault and trial spawner block logic

This commit is contained in:
Alexander Medvedev 2026-08-12 20:18:29 +02:00
parent 55312adfa1
commit d0cc8a2b51
5 changed files with 301 additions and 3 deletions

View File

@ -110,3 +110,5 @@ pub mod coral;
pub mod abstract_wall_mounting;
pub mod beacon;
pub mod trial_spawner;
pub mod vault;

View File

@ -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
})
}
}

View File

@ -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::<VaultBlockEntity>()
{
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
})
}
}

View File

@ -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<Option<NbtCompound>>,
pub server_data: Mutex<Option<NbtCompound>>,
pub rewarded_players: Mutex<HashSet<Uuid>>,
}
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);
}
}

View File

@ -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<BlockRegistry> {
manager.register(SugarCaneBlock);
manager.register(VineBlock);
manager.register(TNTBlock);
manager.register(TrialSpawnerBlock);
manager.register(VaultBlock);
manager.register(BushBlock);
manager.register(FlowerBlock);
manager.register(PotatoBlock);