Skip to main content

steel_core/behavior/blocks/building/
weathering_block.rs

1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::{
5    REGISTRY,
6    blocks::{BlockRef, block_state_ext::BlockStateExt},
7};
8use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
9
10use crate::{
11    behavior::{
12        BlockBehavior, BlockPlaceContext,
13        weathering::{get_weather_state, next_copper_stage},
14    },
15    world::World,
16};
17
18/// Oxidation stages for copper blocks, matching vanilla's `WeatheringCopper.WeatherState`.
19///
20/// Ordinal values are used for age comparisons during the neighbor scan algorithm.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
22#[repr(u8)]
23pub enum WeatherState {
24    /// Fresh copper, no oxidation.
25    Unaffected = 0,
26    /// First stage of oxidation.
27    Exposed = 1,
28    /// Second stage of oxidation.
29    Weathered = 2,
30    /// Fully oxidized, will not advance further.
31    Oxidized = 3,
32}
33
34/// Scan radius for neighbor copper blocks (Manhattan distance).
35const SCAN_DISTANCE: i32 = 4;
36
37/// Base probability per random tick that a copper block even attempts to oxidize.
38/// Vanilla: `0.05688889F` — roughly once per in-game day per block.
39const BASE_CHANCE: f32 = 0.056_888_89;
40
41/// Composable helper for copper weathering/oxidation logic.
42///
43/// Add this as a field to block implementations that should support weathering.
44///
45/// In `YourBlock::random_tick` call [`WeatheringCopper::change_over_time`]
46// TODO: Add weathering support for lanterns, chests, golem statues, and lightning rods.
47pub struct WeatheringCopper {
48    weather_state: WeatherState,
49}
50
51impl WeatheringCopper {
52    /// Creates a new `WeatheringCopper` helper with the given oxidation stage.
53    #[must_use]
54    pub const fn new(weather_state: WeatherState) -> Self {
55        Self { weather_state }
56    }
57
58    /// Advances the weathering state and replaces the block, with a 5.7% chance.
59    ///
60    /// Vanilla: [`ChangeOverTimeBlock.changeOverTime`]
61    pub fn change_over_time(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
62        if rand::random::<f32>() >= BASE_CHANCE {
63            return;
64        }
65
66        if let Some(next_state) = self.get_next_state(state, world, pos) {
67            world.set_block(pos, next_state, UpdateFlags::UPDATE_ALL);
68        }
69    }
70
71    /// Checks the neighbors and calculates the next [`BlockStateId`] for the weathering copper.
72    ///
73    /// 1. Scan Manhattan distance 4 for copper neighbors
74    /// 2. If any younger neighbor exists, abort
75    /// 3. Probability = ((older+1)/(older+same+1))² × `chance_modifier`
76    /// 4. On success, advance to next oxidation stage preserving block properties
77    ///
78    /// Vanilla: `ChangeOverTimeBlock.getNextState`
79    fn get_next_state(
80        &self,
81        state: BlockStateId,
82        world: &Arc<World>,
83        pos: BlockPos,
84    ) -> Option<BlockStateId> {
85        let own_age = self.weather_state as i32;
86        let mut same_age_count = 0i32;
87        let mut older_count = 0i32;
88
89        for dx in -SCAN_DISTANCE..=SCAN_DISTANCE {
90            for dy in -SCAN_DISTANCE..=SCAN_DISTANCE {
91                for dz in -SCAN_DISTANCE..=SCAN_DISTANCE {
92                    if dx.abs() + dy.abs() + dz.abs() > SCAN_DISTANCE {
93                        continue;
94                    }
95                    if dx == 0 && dy == 0 && dz == 0 {
96                        continue;
97                    }
98
99                    let neighbor_pos = BlockPos::new(pos.x() + dx, pos.y() + dy, pos.z() + dz);
100                    let neighbor_state = world.get_block_state(neighbor_pos);
101                    let neighbor_block = neighbor_state.get_block();
102
103                    let Some(neighbor_age) = get_weather_state(neighbor_block) else {
104                        continue;
105                    };
106                    let found_age = neighbor_age as i32;
107
108                    if found_age < own_age {
109                        return None;
110                    }
111
112                    if found_age > own_age {
113                        older_count += 1;
114                    } else {
115                        same_age_count += 1;
116                    }
117                }
118            }
119        }
120
121        let chance = (older_count + 1) as f32 / (older_count + same_age_count + 1) as f32;
122        let actual_chance = chance * chance * self.get_chance_modifier();
123
124        if rand::random::<f32>() >= actual_chance {
125            return None;
126        }
127
128        let old_block = state.get_block();
129        let new_block = next_copper_stage(old_block)?;
130        Some(REGISTRY.blocks.copy_matching_properties(state, new_block))
131    }
132
133    fn get_chance_modifier(&self) -> f32 {
134        if self.weather_state == WeatherState::Unaffected {
135            0.75
136        } else {
137            1.0
138        }
139    }
140}
141
142/// Block behavior for `WeatheringCopperFullBlock`
143///
144/// See [`WeatherState`]
145#[block_behavior]
146pub struct WeatheringCopperFullBlock {
147    block: BlockRef,
148    #[json_arg(r#enum = "WeatherState", json = "weather_state")]
149    weathering: WeatheringCopper,
150}
151
152impl WeatheringCopperFullBlock {
153    /// Creates a new `WeatheringCopperFullBlock` behavior.
154    #[must_use]
155    pub const fn new(block: BlockRef, weather_state: WeatherState) -> Self {
156        Self {
157            block,
158            weathering: WeatheringCopper::new(weather_state),
159        }
160    }
161}
162
163impl BlockBehavior for WeatheringCopperFullBlock {
164    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
165        Some(self.block.default_state())
166    }
167
168    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
169        self.weathering.change_over_time(state, world, pos);
170    }
171}