steel_core/worldgen/feature/features/
end_spike.rs1use std::f64::consts::PI;
2use std::sync::Arc;
3
4use glam::DVec3;
5use steel_math::DEGREE_360;
6use steel_registry::vanilla_entities;
7
8use super::super::prelude::*;
9use super::super::runner::FeatureDecorationRunner;
10use crate::entity::{Entity, entities::EndCrystalEntity, next_entity_id};
11
12const END_SPIKE_COUNT: usize = 10;
13const END_SPIKE_DISTANCE: f64 = 42.0;
14const END_SPIKE_ANGLE_STEP: f64 = PI / 10.0;
15const END_SPIKE_CLEAR_AIR_MIN_Y: i32 = 65;
16const END_SPIKE_CAGE_RADIUS: i32 = 2;
17const END_SPIKE_CAGE_HEIGHT: i32 = 3;
18
19impl FeatureDecorationRunner {
20 pub(in crate::worldgen::feature) fn place_end_spike_feature(
21 region: &mut WorldGenRegion<'_>,
22 random: &mut WorldgenRandom,
23 config: &EndSpikeConfiguration,
24 origin: BlockPos,
25 ) -> bool {
26 let generated_spikes;
27 let spikes = if config.spikes.is_empty() {
28 generated_spikes = Self::end_spikes_for_level(region.seed());
29 &generated_spikes
30 } else {
31 &config.spikes
32 };
33
34 for spike in spikes {
35 if Self::end_spike_center_is_within_chunk(spike, origin) {
36 Self::place_end_spike(region, random, config, spike);
37 }
38 }
39
40 true
41 }
42
43 fn end_spikes_for_level(seed: i64) -> Vec<EndSpike> {
44 let mut seed_random = LegacyRandom::from_seed(seed as u64);
45 let cache_key = seed_random.next_i64() & 65_535;
46 let mut random = LegacyRandom::from_seed(cache_key as u64);
47 let mut sizes = [0_i32; END_SPIKE_COUNT];
48 for (index, size) in sizes.iter_mut().enumerate() {
49 *size = index as i32;
50 }
51 for bound in (2..=END_SPIKE_COUNT).rev() {
52 let swap_to = random.next_i32_bounded(bound as i32) as usize;
53 sizes.swap(bound - 1, swap_to);
54 }
55
56 sizes
57 .iter()
58 .enumerate()
59 .map(|(index, size)| {
60 let angle = 2.0 * (-PI + END_SPIKE_ANGLE_STEP * index as f64);
61 EndSpike {
62 center_x: fast_floor(END_SPIKE_DISTANCE * angle.cos()),
63 center_z: fast_floor(END_SPIKE_DISTANCE * angle.sin()),
64 radius: 2 + size / 3,
65 height: 76 + size * 3,
66 guarded: *size == 1 || *size == 2,
67 }
68 })
69 .collect()
70 }
71
72 const fn end_spike_center_is_within_chunk(spike: &EndSpike, origin: BlockPos) -> bool {
73 SectionPos::block_to_section_coord(origin.x())
74 == SectionPos::block_to_section_coord(spike.center_x)
75 && SectionPos::block_to_section_coord(origin.z())
76 == SectionPos::block_to_section_coord(spike.center_z)
77 }
78
79 fn place_end_spike(
80 region: &mut WorldGenRegion<'_>,
81 random: &mut WorldgenRandom,
82 config: &EndSpikeConfiguration,
83 spike: &EndSpike,
84 ) {
85 Self::place_end_spike_body(region, spike);
86 if spike.guarded {
87 Self::place_end_spike_cage(region, spike);
88 }
89 Self::place_end_spike_crystal(region, random, config, spike);
90 }
91
92 fn place_end_spike_body(region: &mut WorldGenRegion<'_>, spike: &EndSpike) {
93 let radius_squared_plus_one = spike.radius * spike.radius + 1;
94 let obsidian = REGISTRY
95 .blocks
96 .get_default_state_id(&vanilla_blocks::OBSIDIAN);
97 let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
98
99 for x in (spike.center_x - spike.radius)..=(spike.center_x + spike.radius) {
100 for y in region.min_y()..=(spike.height + 10) {
101 for z in (spike.center_z - spike.radius)..=(spike.center_z + spike.radius) {
102 let pos = BlockPos::new(x, y, z);
103 if Self::end_spike_inside_radius(spike, pos, radius_squared_plus_one)
104 && y < spike.height
105 {
106 let _ = region.set_block_state(pos, obsidian, UpdateFlags::UPDATE_ALL);
107 } else if y > END_SPIKE_CLEAR_AIR_MIN_Y {
108 let _ = region.set_block_state(pos, air, UpdateFlags::UPDATE_ALL);
109 }
110 }
111 }
112 }
113 }
114
115 fn end_spike_inside_radius(
116 spike: &EndSpike,
117 pos: BlockPos,
118 radius_squared_plus_one: i32,
119 ) -> bool {
120 let dx = f64::from(pos.x() - spike.center_x);
121 let dz = f64::from(pos.z() - spike.center_z);
122 dx * dx + dz * dz <= f64::from(radius_squared_plus_one)
123 }
124
125 fn place_end_spike_cage(region: &mut WorldGenRegion<'_>, spike: &EndSpike) {
126 for dx in -END_SPIKE_CAGE_RADIUS..=END_SPIKE_CAGE_RADIUS {
127 for dz in -END_SPIKE_CAGE_RADIUS..=END_SPIKE_CAGE_RADIUS {
128 for dy in 0..=END_SPIKE_CAGE_HEIGHT {
129 let touches_width_limit = dx.abs() == END_SPIKE_CAGE_RADIUS;
130 let touches_depth_limit = dz.abs() == END_SPIKE_CAGE_RADIUS;
131 let top = dy == END_SPIKE_CAGE_HEIGHT;
132 if !touches_width_limit && !touches_depth_limit && !top {
133 continue;
134 }
135
136 let x_edge = touches_width_limit || top;
137 let z_edge = touches_depth_limit || top;
138 let state = Self::end_spike_iron_bars_state(x_edge, z_edge, dx, dz);
139 let pos =
140 BlockPos::new(spike.center_x + dx, spike.height + dy, spike.center_z + dz);
141 let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_ALL);
142 }
143 }
144 }
145 }
146
147 fn end_spike_iron_bars_state(x_edge: bool, z_edge: bool, dx: i32, dz: i32) -> BlockStateId {
148 REGISTRY
149 .blocks
150 .get_default_state_id(&vanilla_blocks::IRON_BARS)
151 .set_value(
152 &BlockStateProperties::NORTH,
153 x_edge && dz != -END_SPIKE_CAGE_RADIUS,
154 )
155 .set_value(
156 &BlockStateProperties::SOUTH,
157 x_edge && dz != END_SPIKE_CAGE_RADIUS,
158 )
159 .set_value(
160 &BlockStateProperties::WEST,
161 z_edge && dx != -END_SPIKE_CAGE_RADIUS,
162 )
163 .set_value(
164 &BlockStateProperties::EAST,
165 z_edge && dx != END_SPIKE_CAGE_RADIUS,
166 )
167 }
168
169 fn place_end_spike_crystal(
170 region: &mut WorldGenRegion<'_>,
171 random: &mut WorldgenRandom,
172 config: &EndSpikeConfiguration,
173 spike: &EndSpike,
174 ) {
175 let position = DVec3::new(
176 f64::from(spike.center_x) + 0.5,
177 f64::from(spike.height + 1),
178 f64::from(spike.center_z) + 0.5,
179 );
180 let crystal = Arc::new(EndCrystalEntity::new(
181 &vanilla_entities::END_CRYSTAL,
182 next_entity_id(),
183 position,
184 region.weak_world(),
185 ));
186 crystal.set_beam_target(config.crystal_beam_target.map(BlockPos));
187 crystal.set_invulnerable(config.crystal_invulnerable);
188 crystal.snap_to(position, random.next_f32() * DEGREE_360, 0.0);
189 let _ = region.add_fresh_entity(crystal);
190
191 let crystal_pos = BlockPos::from(position);
192 let bedrock = REGISTRY
193 .blocks
194 .get_default_state_id(&vanilla_blocks::BEDROCK);
195 let fire = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::FIRE);
196 let _ = region.set_block_state(crystal_pos.below(), bedrock, UpdateFlags::UPDATE_ALL);
197 let _ = region.set_block_state(crystal_pos, fire, UpdateFlags::UPDATE_ALL);
198 }
199}