feat(chunk-gen) add scattered ore, twisting vines, blue ice

This commit is contained in:
Alexander Medvedev
2026-05-06 18:59:36 +02:00
parent 852057aaf6
commit fd56a5be7a
7 changed files with 361 additions and 16 deletions

View File

@@ -226,7 +226,9 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream {
if type_str == "minecraft:scattered_ore" {
quote! {
ConfiguredFeature::ScatteredOre(crate::generation::feature::features::scattered_ore::ScatteredOreFeature {
// TODO
size: #size,
discard_chance_on_air_exposure: #discard,
targets: vec![#(#targets),*],
})
}
} else {
@@ -701,7 +703,16 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream {
quote! { ConfiguredFeature::WeepingVines(crate::generation::feature::features::weeping_vines::WeepingVinesFeature {}) }
}
"minecraft:twisting_vines" => {
quote! { ConfiguredFeature::TwistingVines(crate::generation::feature::features::twisting_vines::TwistingVinesFeature {}) }
let spread_width = config["spread_width"].as_i64().unwrap_or(0) as i32;
let spread_height = config["spread_height"].as_i64().unwrap_or(0) as i32;
let max_height = config["max_height"].as_i64().unwrap_or(0) as i32;
quote! {
ConfiguredFeature::TwistingVines(crate::generation::feature::features::twisting_vines::TwistingVinesFeature {
spread_width: #spread_width,
spread_height: #spread_height,
max_height: #max_height,
})
}
}
"minecraft:delta_feature" => {
quote! { ConfiguredFeature::DeltaFeature(crate::generation::feature::features::delta_feature::DeltaFeatureFeature {}) }

View File

@@ -2738,13 +2738,31 @@ fn build_configured_features() -> std::collections::HashMap<String, ConfiguredFe
map.insert(
"ore_ancient_debris_large".to_string(),
ConfiguredFeature::ScatteredOre(
crate::generation::feature::features::scattered_ore::ScatteredOreFeature {},
crate::generation::feature::features::scattered_ore::ScatteredOreFeature {
size: 3i32,
discard_chance_on_air_exposure: 1f32,
targets: vec![OreTarget {
target: RuleTest::TagMatch(TagMatchRuleTest {
tag: pumpkin_data::tag::Block::MINECRAFT_BASE_STONE_NETHER,
}),
state: pumpkin_data::Block::ANCIENT_DEBRIS.default_state,
}],
},
),
);
map.insert(
"ore_ancient_debris_small".to_string(),
ConfiguredFeature::ScatteredOre(
crate::generation::feature::features::scattered_ore::ScatteredOreFeature {},
crate::generation::feature::features::scattered_ore::ScatteredOreFeature {
size: 2i32,
discard_chance_on_air_exposure: 1f32,
targets: vec![OreTarget {
target: RuleTest::TagMatch(TagMatchRuleTest {
tag: pumpkin_data::tag::Block::MINECRAFT_BASE_STONE_NETHER,
}),
state: pumpkin_data::Block::ANCIENT_DEBRIS.default_state,
}],
},
),
);
map.insert(
@@ -4234,13 +4252,21 @@ fn build_configured_features() -> std::collections::HashMap<String, ConfiguredFe
map.insert(
"twisting_vines".to_string(),
ConfiguredFeature::TwistingVines(
crate::generation::feature::features::twisting_vines::TwistingVinesFeature {},
crate::generation::feature::features::twisting_vines::TwistingVinesFeature {
spread_width: 8i32,
spread_height: 4i32,
max_height: 8i32,
},
),
);
map.insert(
"twisting_vines_bonemeal".to_string(),
ConfiguredFeature::TwistingVines(
crate::generation::feature::features::twisting_vines::TwistingVinesFeature {},
crate::generation::feature::features::twisting_vines::TwistingVinesFeature {
spread_width: 3i32,
spread_height: 1i32,
max_height: 2i32,
},
),
);
map.insert(

View File

@@ -317,6 +317,9 @@ impl ConfiguredFeature {
Self::Seagrass(feature) => {
feature.generate(chunk, min_y, height, feature_name, random, pos)
}
Self::TwistingVines(feature) => {
feature.generate(chunk, min_y, height, feature_name, random, pos)
}
Self::UnderwaterMagma(feature) => {
feature.generate(chunk, min_y, height, feature_name, random, pos)
}
@@ -344,9 +347,19 @@ impl ConfiguredFeature {
random,
pos,
),
Self::ScatteredOre(feature) => feature.generate(
chunk,
block_registry,
min_y,
height,
feature_name,
random,
pos,
),
Self::MonsterRoom(feature) => {
feature.generate(chunk, min_y, height, feature_name, random, pos)
}
Self::BlueIce(feature) => feature.generate(chunk, random, pos),
Self::GlowstoneBlob(feature) => {
feature.generate(chunk, min_y, height, feature_name, random, pos)
}

View File

@@ -1,3 +1,95 @@
pub struct BlueIceFeature {
// TODO
use pumpkin_data::Block;
use pumpkin_util::{
math::position::BlockPos,
random::{RandomGenerator, RandomImpl},
};
use crate::generation::proto_chunk::GenerationCache;
const SEA_LEVEL: i32 = 63; // TODO: use getSeaLevel() instead of hardcoding
pub struct BlueIceFeature {}
impl BlueIceFeature {
pub fn generate<T: GenerationCache>(
&self,
chunk: &mut T,
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
if pos.0.y >= SEA_LEVEL - 1 {
return false;
}
let block = GenerationCache::get_block_state(chunk, &pos.0).to_state();
let block_below = GenerationCache::get_block_state(chunk, &pos.down().0).to_state();
if block != Block::WATER.default_state && block_below != Block::WATER.default_state {
return false;
}
let mut has_ice_neighbor = false;
for neighbor_pos in [
pos.up(),
pos.down(),
pos.north(),
pos.south(),
pos.east(),
pos.west(),
] {
if GenerationCache::get_block_state(chunk, &neighbor_pos.0).to_state()
== Block::ICE.default_state
{
has_ice_neighbor = true;
break;
}
}
if !has_ice_neighbor {
return false;
}
chunk.set_block_state(&pos.0, Block::BLUE_ICE.default_state);
for _ in 0..200 {
let offset_x = random.next_bounded_i32(8) - random.next_bounded_i32(8);
let offset_y = random.next_bounded_i32(8) - random.next_bounded_i32(8);
let offset_z = random.next_bounded_i32(8) - random.next_bounded_i32(8);
let target_pos = pos.add(offset_x, offset_y, offset_z);
if chunk.out_of_height(target_pos.0.y as i16) {
continue;
}
let target_state = GenerationCache::get_block_state(chunk, &target_pos.0).to_state();
if target_state.is_air()
|| target_state == Block::WATER.default_state
|| target_state == Block::ICE.default_state
|| target_state == Block::PACKED_ICE.default_state
{
let mut has_blue_ice_neighbor = false;
for neighbor_pos in [
target_pos.up(),
target_pos.down(),
target_pos.north(),
target_pos.south(),
target_pos.east(),
target_pos.west(),
] {
if GenerationCache::get_block_state(chunk, &neighbor_pos.0).to_state()
== Block::BLUE_ICE.default_state
{
has_blue_ice_neighbor = true;
break;
}
}
if has_blue_ice_neighbor {
chunk.set_block_state(&target_pos.0, Block::BLUE_ICE.default_state);
}
}
}
true
}
}

View File

@@ -184,7 +184,14 @@ impl OreFeature {
let block_state = GenerationCache::get_block_state(chunk, &pos_vec);
for target in &self.targets {
if self.should_place(chunk, block_state, random, target, &mutable_pos) {
if Self::should_place(
self.discard_chance_on_air_exposure,
chunk,
block_state,
random,
target,
&mutable_pos,
) {
chunk.set_block_state(&pos_vec, target.state);
placed_blocks_count += 1;
break; // Equivalent to 'continue block11;'
@@ -197,8 +204,8 @@ impl OreFeature {
placed_blocks_count > 0
}
fn should_place<T: GenerationCache>(
&self,
pub fn should_place<T: GenerationCache>(
discard_chance: f32,
chunk: &T,
state: RawBlockState,
random: &mut RandomGenerator,
@@ -208,13 +215,13 @@ impl OreFeature {
if !target.target.test(state, random) {
return false;
}
if Self::should_not_discard(random, self.discard_chance_on_air_exposure) {
if Self::should_not_discard(random, discard_chance) {
return true;
}
!Self::is_exposed_to_air(chunk, pos)
}
fn should_not_discard(random: &mut RandomGenerator, chance: f32) -> bool {
pub fn should_not_discard(random: &mut RandomGenerator, chance: f32) -> bool {
if chance <= 0.0f32 {
return true;
}
@@ -224,7 +231,7 @@ impl OreFeature {
random.next_f32() >= chance
}
fn is_exposed_to_air<T: GenerationCache>(chunk: &T, pos: &BlockPos) -> bool {
pub fn is_exposed_to_air<T: GenerationCache>(chunk: &T, pos: &BlockPos) -> bool {
for dir in BlockDirection::all() {
if GenerationCache::get_block_state(chunk, &pos.offset(dir.to_offset()).0)
.to_state()

View File

@@ -1,3 +1,69 @@
use pumpkin_util::{
math::position::BlockPos,
random::{RandomGenerator, RandomImpl},
};
use crate::generation::proto_chunk::GenerationCache;
use crate::world::BlockRegistryExt;
use super::ore::{OreFeature, OreTarget};
pub struct ScatteredOreFeature {
// TODO
pub size: i32,
pub discard_chance_on_air_exposure: f32,
pub targets: Vec<OreTarget>,
}
impl ScatteredOreFeature {
#[expect(clippy::too_many_arguments)]
pub fn generate<T: GenerationCache>(
&self,
chunk: &mut T,
_block_registry: &dyn BlockRegistryExt,
_min_y: i8,
_height: u16,
_feature: &str,
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
let count = random.next_bounded_i32(self.size + 1);
let mut placed = false;
for _ in 0..count {
let offset_x = self.get_random_offset(random);
let offset_y = self.get_random_offset(random);
let offset_z = self.get_random_offset(random);
let target_pos = pos.add(offset_x, offset_y, offset_z);
if chunk.out_of_height(target_pos.0.y as i16) {
continue;
}
let block_state = GenerationCache::get_block_state(chunk, &target_pos.0);
for target in &self.targets {
if OreFeature::should_place(
self.discard_chance_on_air_exposure,
chunk,
block_state,
random,
target,
&target_pos,
) {
chunk.set_block_state(&target_pos.0, target.state);
placed = true;
break;
}
}
}
placed
}
fn get_random_offset(&self, random: &mut RandomGenerator) -> i32 {
let f1 = random.next_f32();
let f2 = random.next_f32();
((f1 - f2) * self.size as f32).round() as i32
}
}

View File

@@ -1,3 +1,133 @@
use pumpkin_data::Block;
use pumpkin_util::{
math::position::BlockPos,
random::{RandomGenerator, RandomImpl},
};
use crate::generation::proto_chunk::GenerationCache;
pub struct TwistingVinesFeature {
// TODO
pub spread_width: i32,
pub spread_height: i32,
pub max_height: i32,
}
impl TwistingVinesFeature {
pub fn generate<T: GenerationCache>(
&self,
chunk: &mut T,
_min_y: i8,
_height: u16,
_feature_name: &str,
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
if self.is_invalid_location(chunk, &pos) {
return false;
}
let mut placed = false;
for _ in 0..self.spread_width * self.spread_width {
let offset_x = random.next_bounded_i32(self.spread_width)
- random.next_bounded_i32(self.spread_width);
let offset_y = random.next_bounded_i32(self.spread_height)
- random.next_bounded_i32(self.spread_height);
let offset_z = random.next_bounded_i32(self.spread_width)
- random.next_bounded_i32(self.spread_width);
let mut mutable_pos = pos.add(offset_x, offset_y, offset_z);
if self.find_target_y(chunk, &mut mutable_pos)
&& !self.is_invalid_location(chunk, &mutable_pos)
{
let mut height = random.next_bounded_i32(self.max_height) + 1;
if random.next_bounded_i32(6) == 0 {
height *= 2;
}
if random.next_bounded_i32(10) == 0 {
height = 1;
}
self.generate_column(chunk, random, &mutable_pos, height);
placed = true;
}
}
placed
}
fn generate_column<T: GenerationCache>(
&self,
chunk: &mut T,
random: &mut RandomGenerator,
pos: &BlockPos,
height: i32,
) {
let mut current_pos = *pos;
for i in 0..height {
if !GenerationCache::get_block_state(chunk, &current_pos.0)
.to_state()
.is_air()
{
break;
}
if i == height - 1
|| !GenerationCache::get_block_state(chunk, &current_pos.up().0)
.to_state()
.is_air()
{
// Top part
let _age = 17 + random.next_bounded_i32(25 - 17 + 1);
// We should set the age property here, but Pumpkin's BlockState might not support it easily yet or we just use default
// For now, let's just use the block.
// TODO: Set age property
chunk.set_block_state(&current_pos.0, Block::TWISTING_VINES.default_state);
break;
} else {
chunk.set_block_state(&current_pos.0, Block::TWISTING_VINES_PLANT.default_state);
}
current_pos = current_pos.up();
}
}
fn is_invalid_location<T: GenerationCache>(&self, chunk: &T, pos: &BlockPos) -> bool {
if !GenerationCache::get_block_state(chunk, &pos.0)
.to_state()
.is_air()
{
return true;
}
let block_below = GenerationCache::get_block_state(chunk, &pos.down().0).to_state();
block_below != Block::WARPED_NYLIUM.default_state
&& block_below != Block::WARPED_WART_BLOCK.default_state
&& block_below != Block::TWISTING_VINES.default_state
&& block_below != Block::TWISTING_VINES_PLANT.default_state
}
fn find_target_y<T: GenerationCache>(&self, chunk: &T, pos: &mut BlockPos) -> bool {
// Try to find a valid floor by looking down
let mut current = *pos;
for _ in 0..self.spread_height {
if GenerationCache::get_block_state(chunk, &current.0)
.to_state()
.is_air()
{
let below = current.down();
let block_below = GenerationCache::get_block_state(chunk, &below.0).to_state();
if block_below == Block::WARPED_NYLIUM.default_state
|| block_below == Block::WARPED_WART_BLOCK.default_state
|| block_below == Block::TWISTING_VINES.default_state
|| block_below == Block::TWISTING_VINES_PLANT.default_state
{
*pos = current;
return true;
}
}
current = current.down();
}
false
}
}