feat: impl some Sculk blocks

This commit is contained in:
Alexander Medvedev
2026-08-09 22:41:09 +02:00
parent 6720ac265e
commit 5bc8077848
7 changed files with 492 additions and 0 deletions

View File

@@ -77,6 +77,7 @@ pub mod logs;
pub mod mangrove_roots;
pub mod plant;
pub mod pumpkin;
pub mod sculk;
pub mod vine;
// Terrain / environment / physics
@@ -98,6 +99,7 @@ pub mod cake;
pub mod cauldron;
pub mod composter;
pub mod ladder;
pub mod respawn_anchor;
pub mod slime;
pub mod spawner;
pub mod tnt;

View File

@@ -0,0 +1,119 @@
use pumpkin_data::block_properties::{BlockProperties, RespawnAnchorLikeProperties};
use pumpkin_data::dimension::Dimension;
use pumpkin_data::item::Item;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::translation;
use pumpkin_macros::pumpkin_block;
use pumpkin_util::text::TextComponent;
use pumpkin_world::world::BlockFlags;
use crate::block::registry::BlockActionResult;
use crate::block::{BlockBehaviour, BlockFuture, NormalUseArgs, UseWithItemArgs};
use crate::entity::EntityBase;
#[pumpkin_block("minecraft:respawn_anchor")]
pub struct RespawnAnchorBlock;
impl BlockBehaviour for RespawnAnchorBlock {
fn use_with_item<'a>(
&'a self,
args: UseWithItemArgs<'a>,
) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move {
if args.item_stack.item.id != Item::GLOWSTONE.id {
return BlockActionResult::Pass;
}
let state_id = args.world.get_block_state_id(args.position);
let mut props = RespawnAnchorLikeProperties::from_state_id(state_id, args.block);
if props.charges >= 4 {
return BlockActionResult::Pass;
}
props.charges += 1;
args.world
.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_ALL,
)
.await;
args.item_stack
.decrement_unless_creative(args.player.gamemode.load(), 1);
args.world.play_sound(
Sound::BlockRespawnAnchorCharge,
SoundCategory::Blocks,
&args.position.to_f64(),
);
BlockActionResult::Success
})
}
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 = RespawnAnchorLikeProperties::from_state_id(state_id, args.block);
if args.world.dimension != Dimension::THE_NETHER {
args.world
.break_block(args.position, None, BlockFlags::SKIP_DROPS)
.await;
args.world
.explode(args.position.to_centered_f64(), 5.0)
.await;
return BlockActionResult::SuccessServer;
}
if props.charges == 0 {
args.player
.send_system_message(&TextComponent::translate(
translation::java::BLOCK_MINECRAFT_BED_NO_SLEEP,
&[],
))
.await;
return BlockActionResult::SuccessServer;
}
if args
.player
.set_respawn_point(
args.world.dimension.clone(),
*args.position,
args.player.get_entity().yaw.load(),
args.player.get_entity().pitch.load(),
false,
)
.await
{
props.charges -= 1;
args.world
.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_ALL,
)
.await;
args.world.play_sound(
Sound::BlockRespawnAnchorSetSpawn,
SoundCategory::Blocks,
&args.position.to_f64(),
);
args.player
.send_system_message(&TextComponent::translate_cross(
translation::java::BLOCK_MINECRAFT_SET_SPAWN,
translation::bedrock::TILE_BED_RESPAWNSET,
[],
))
.await;
}
BlockActionResult::SuccessServer
})
}
}

View File

@@ -0,0 +1,3 @@
pub mod sculk_catalyst;
pub mod sculk_shrieker;
pub mod sculk_vein;

View File

@@ -0,0 +1,23 @@
use crate::block::{BlockBehaviour, BlockFuture, BlockMetadata, OnPlaceArgs};
use pumpkin_data::{
BlockId, BlockStateId,
block_properties::{BlockProperties, SculkCatalystLikeProperties},
};
pub struct SculkCatalystBlock;
impl BlockMetadata for SculkCatalystBlock {
fn ids() -> Box<[BlockId]> {
[BlockId::SCULK_CATALYST].into()
}
}
impl BlockBehaviour for SculkCatalystBlock {
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move {
let mut props = SculkCatalystLikeProperties::default(args.block);
props.bloom = false;
props.to_state_id(args.block)
})
}
}

View File

@@ -0,0 +1,110 @@
use std::sync::Arc;
use crate::block::entities::sculk_shrieker::SculkShriekerBlockEntity;
use crate::block::{BlockBehaviour, BlockFuture, BlockMetadata, OnPlaceArgs, OnScheduledTickArgs};
use crate::world::World;
use pumpkin_data::potion::Effect;
use pumpkin_data::{
BlockId, BlockStateId,
block_properties::{BlockProperties, SculkShriekerLikeProperties},
effect::StatusEffect,
sound::{Sound, SoundCategory},
};
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::tick::TickPriority;
use pumpkin_world::world::BlockFlags;
const SHRIEK_TICKS: u8 = 90;
const DARKNESS_RADIUS: f64 = 40.0;
const DARKNESS_DURATION: i32 = 260;
pub struct SculkShriekerBlock;
impl BlockMetadata for SculkShriekerBlock {
fn ids() -> Box<[BlockId]> {
[BlockId::SCULK_SHRIEKER].into()
}
}
impl SculkShriekerBlock {
pub async fn try_activate(world: &Arc<World>, pos: &BlockPos) -> bool {
let block = world.get_block(pos);
if block.id != BlockId::SCULK_SHRIEKER {
return false;
}
let state = world.get_block_state(pos);
let mut props = SculkShriekerLikeProperties::from_state_id(state.id, block);
if props.shrieking {
return false;
}
props.shrieking = true;
world
.set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_ALL)
.await;
world.play_sound(
Sound::BlockSculkShriekerShriek,
SoundCategory::Blocks,
&pos.to_f64(),
);
world.schedule_block_tick(block, *pos, SHRIEK_TICKS, TickPriority::Normal);
let center = pos.to_centered_f64();
for player in world.get_nearby_players(center, DARKNESS_RADIUS) {
let darkness = Effect {
effect_type: &StatusEffect::DARKNESS,
duration: DARKNESS_DURATION,
amplifier: 0,
ambient: false,
show_particles: false,
show_icon: true,
blend: true,
};
player.send_effect(darkness.clone()).await;
player.living_entity.add_effect(darkness).await;
}
if let Some(entity) = world.get_block_entity(pos)
&& let Some(shrieker) = entity.as_any().downcast_ref::<SculkShriekerBlockEntity>()
{
let mut level = shrieker.warning_level.lock().await;
*level = (*level + 1).min(4);
if props.can_summon && *level >= 4 {
// TODO: spawn Warden near pos
}
}
true
}
}
impl BlockBehaviour for SculkShriekerBlock {
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move {
let mut props = SculkShriekerLikeProperties::default(args.block);
props.shrieking = false;
props.waterlogged = args.replacing.water_source();
props.to_state_id(args.block)
})
}
fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move {
let state = args.world.get_block_state(args.position);
let mut props = SculkShriekerLikeProperties::from_state_id(state.id, args.block);
if props.shrieking {
props.shrieking = false;
args.world
.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_ALL,
)
.await;
}
})
}
}

View File

@@ -0,0 +1,227 @@
use std::collections::HashSet;
use crate::block::{
BlockBehaviour, BlockFuture, BlockIsReplacing, BlockMetadata, CanPlaceAtArgs, CanUpdateAtArgs,
GetStateForNeighborUpdateArgs, OnPlaceArgs, UseWithItemArgs, registry::BlockActionResult,
};
use crate::entity::{EntityBase, player::Player};
use pumpkin_data::{
Block, BlockDirection, BlockId, BlockStateId, FacingExt,
block_properties::{BlockProperties, GlowLichenLikeProperties},
item::Item,
};
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::{BlockAccessor, BlockFlags};
pub struct SculkVeinBlock;
impl BlockMetadata for SculkVeinBlock {
fn ids() -> Box<[BlockId]> {
[BlockId::SCULK_VEIN].into()
}
}
impl BlockBehaviour for SculkVeinBlock {
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move {
if let BlockIsReplacing::Itself(state_id) = args.replacing {
let (Some(direction), _) = get_attach_direction(
args.world,
args.position,
Some(args.player),
args.direction,
true,
) else {
return Block::AIR.default_state.id;
};
let mut props = GlowLichenLikeProperties::from_state_id(state_id, args.block);
set_face(&mut props, direction);
props.waterlogged = args.replacing.water_source();
return props.to_state_id(args.block);
}
let (Some(direction), _) = get_attach_direction(
args.world,
args.position,
Some(args.player),
args.direction,
false,
) else {
return Block::AIR.default_state.id;
};
let mut props = GlowLichenLikeProperties::default(args.block);
set_face(&mut props, direction);
props.waterlogged = args.replacing.water_source();
props.to_state_id(args.block)
})
}
fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool {
get_attach_direction(
args.block_accessor,
args.position,
args.player,
args.direction.unwrap_or(BlockDirection::Down),
false,
)
.0
.is_some()
}
fn can_update_at(&self, args: CanUpdateAtArgs<'_>) -> bool {
get_attach_direction(
args.world,
args.position,
Some(args.player),
args.direction,
true,
)
.0
.is_some()
}
fn get_state_for_neighbor_update<'a>(
&'a self,
args: GetStateForNeighborUpdateArgs<'a>,
) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move {
let old_props = GlowLichenLikeProperties::from_state_id(args.state_id, args.block);
let mut new_directions = active_directions(old_props);
let support = args
.world
.get_block(&args.position.offset(args.direction.to_offset()));
if !is_solid_face(support) {
new_directions.remove(&args.direction);
}
if new_directions.is_empty() {
return Block::AIR.default_state.id;
}
let mut new_props = GlowLichenLikeProperties::default(args.block);
for dir in new_directions {
set_face(&mut new_props, dir);
}
new_props.to_state_id(args.block)
})
}
fn use_with_item<'a>(
&'a self,
args: UseWithItemArgs<'a>,
) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move {
if args.item_stack.item.id != Item::SCULK_VEIN.id {
return BlockActionResult::Pass;
}
let state = args.world.get_block_state(args.position);
let mut props = GlowLichenLikeProperties::from_state_id(state.id, args.block);
let (Some(accurate_dir), _) = get_attach_direction(
args.world.as_ref(),
args.position,
Some(args.player),
*args.hit.face,
true,
) else {
return BlockActionResult::Fail;
};
set_face(&mut props, accurate_dir);
args.world
.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_ALL,
)
.await;
BlockActionResult::Consume
})
}
}
fn get_attach_direction(
block_accessor: &dyn BlockAccessor,
block_pos: &BlockPos,
player_wrapper: Option<&Player>,
click_direction: BlockDirection,
replacing: bool,
) -> (Option<BlockDirection>, bool) {
let clicked_block = block_accessor.get_block(&block_pos.offset(click_direction.to_offset()));
if !replacing && clicked_block == &Block::SCULK_VEIN {
return (None, false);
}
if is_solid_face(clicked_block) {
return (Some(click_direction), false);
}
let (replacing_block, replacing_block_state) = block_accessor.get_block_and_state(block_pos);
let already_active = if replacing_block == &Block::SCULK_VEIN {
active_directions(GlowLichenLikeProperties::from_state_id(
replacing_block_state.id,
replacing_block,
))
} else {
HashSet::new()
};
if let Some(player) = player_wrapper {
let fs = player.get_entity().get_entity_facing_order();
let directions = [
fs[0].to_block_direction(),
fs[1].to_block_direction(),
fs[2].to_block_direction(),
fs[3].to_block_direction(),
fs[4].to_block_direction(),
fs[5].to_block_direction(),
];
for dir in directions {
if !already_active.contains(&dir) {
let support = block_accessor.get_block(&block_pos.offset(dir.to_offset()));
if is_solid_face(support) {
return (Some(dir), false);
}
}
}
}
(None, false)
}
const fn is_solid_face(block: &Block) -> bool {
block.default_state.is_full_cube()
}
fn active_directions(props: GlowLichenLikeProperties) -> HashSet<BlockDirection> {
let mut set = HashSet::new();
if props.down {
set.insert(BlockDirection::Down);
}
if props.up {
set.insert(BlockDirection::Up);
}
if props.north {
set.insert(BlockDirection::North);
}
if props.south {
set.insert(BlockDirection::South);
}
if props.east {
set.insert(BlockDirection::East);
}
if props.west {
set.insert(BlockDirection::West);
}
set
}
const fn set_face(props: &mut GlowLichenLikeProperties, direction: BlockDirection) {
match direction {
BlockDirection::Down => props.down = true,
BlockDirection::Up => props.up = true,
BlockDirection::North => props.north = true,
BlockDirection::South => props.south = true,
BlockDirection::West => props.west = true,
BlockDirection::East => props.east = true,
}
}

View File

@@ -119,6 +119,9 @@ use crate::block::blocks::redstone::sculk_sensor::SculkSensorBlock;
use crate::block::blocks::redstone::target_block::TargetBlock;
use crate::block::blocks::redstone::tripwire::TripwireBlock;
use crate::block::blocks::redstone::tripwire_hook::TripwireHookBlock;
use crate::block::blocks::sculk::sculk_catalyst::SculkCatalystBlock;
use crate::block::blocks::sculk::sculk_shrieker::SculkShriekerBlock;
use crate::block::blocks::sculk::sculk_vein::SculkVeinBlock;
use crate::block::blocks::shelf::ShelfBlock;
use crate::block::blocks::signs::SignBlock;
use crate::block::blocks::slabs::SlabBlock;
@@ -182,6 +185,7 @@ use crate::block::blocks::jukebox::JukeboxBlock;
use crate::block::blocks::ladder::LadderBlock;
use crate::block::blocks::lanterns::LanternBlock;
use crate::block::blocks::lectern::LecternBlock;
use crate::block::blocks::respawn_anchor::RespawnAnchorBlock;
use crate::block::blocks::shulker_box::ShulkerBoxBlock;
use crate::block::blocks::skull_block::SkullBlock;
use crate::block::blocks::smoker::SmokerBlock;
@@ -280,6 +284,7 @@ pub fn default_registry() -> Arc<BlockRegistry> {
manager.register(EndPortalBlock);
manager.register(SpawnerBlock);
manager.register(EndPortalFrameBlock);
manager.register(RespawnAnchorBlock);
manager.register(CandleBlock);
manager.register(SeaPickleBlock);
manager.register(CakeBlock);
@@ -330,6 +335,9 @@ pub fn default_registry() -> Arc<BlockRegistry> {
manager.register(LeverBlock);
manager.register(LightningRodBlock);
manager.register(SculkSensorBlock);
manager.register(SculkVeinBlock);
manager.register(SculkCatalystBlock);
manager.register(SculkShriekerBlock);
manager.register(ObserverBlock);
manager.register(TripwireBlock);
manager.register(TripwireHookBlock);