Skip to main content

steel_core/command/builtins/
weather.rs

1//! Per-world weather command.
2
3use steel_utils::{Identifier, translations};
4use text_components::TextComponent;
5
6use super::super::{
7    brigadier::{CommandNodeBuilder, CommandSyntaxError},
8    execution::{
9        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
10        literal,
11    },
12    registration::CommandRegistration,
13};
14
15const DEFAULT_DURATION: i32 = -1;
16
17pub(super) fn registration() -> CommandRegistration<CommandSource> {
18    CommandRegistration::new(Identifier::vanilla_static("weather"), |_| command())
19}
20
21fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
22    literal("weather")
23        .then(weather_literal("clear", WeatherKind::Clear))
24        .then(weather_literal("rain", WeatherKind::Rain))
25        .then(weather_literal("thunder", WeatherKind::Thunder))
26}
27
28fn weather_literal(
29    name: &'static str,
30    weather: WeatherKind,
31) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
32    literal(name)
33        .executes(move |context| set_weather(context, weather, DEFAULT_DURATION))
34        .then(
35            argument("duration", SteelArgumentType::time(1)).executes(move |context| {
36                let Some(duration) = context.time("duration") else {
37                    return Err(CommandSyntaxError::dynamic(
38                        "Parsed weather duration is missing from the command context",
39                    ));
40                };
41                set_weather(context, weather, duration)
42            }),
43        )
44}
45
46#[derive(Clone, Copy)]
47enum WeatherKind {
48    Clear,
49    Rain,
50    Thunder,
51}
52
53impl WeatherKind {
54    fn random_duration(self) -> i32 {
55        match self {
56            Self::Clear => rand::random_range(12_000..=180_000),
57            Self::Rain => rand::random_range(12_000..=24_000),
58            Self::Thunder => rand::random_range(3_600..=15_600),
59        }
60    }
61
62    const fn parameters(self, duration: i32) -> (i32, i32, bool, bool) {
63        match self {
64            Self::Clear => (duration, 0, false, false),
65            Self::Rain => (0, duration, true, false),
66            Self::Thunder => (0, duration, true, true),
67        }
68    }
69
70    fn success_message(self) -> TextComponent {
71        match self {
72            Self::Clear => TextComponent::from(&translations::COMMANDS_WEATHER_SET_CLEAR),
73            Self::Rain => TextComponent::from(&translations::COMMANDS_WEATHER_SET_RAIN),
74            Self::Thunder => TextComponent::from(&translations::COMMANDS_WEATHER_SET_THUNDER),
75        }
76    }
77}
78
79#[expect(
80    clippy::unnecessary_wraps,
81    reason = "Command executors use a shared fallible callback signature."
82)]
83fn set_weather(
84    context: &SteelCommandContext<CommandSource>,
85    weather: WeatherKind,
86    requested_duration: i32,
87) -> Result<i32, CommandSyntaxError> {
88    let duration = if requested_duration == DEFAULT_DURATION {
89        weather.random_duration()
90    } else {
91        requested_duration
92    };
93    let (clear_time, rain_time, raining, thundering) = weather.parameters(duration);
94    context
95        .source()
96        .world()
97        .set_weather_parameters(clear_time, rain_time, raining, thundering);
98    context
99        .source()
100        .send_success(&weather.success_message(), true);
101    Ok(requested_duration)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::WeatherKind;
107
108    #[test]
109    fn weather_kinds_map_to_vanilla_parameter_sets() {
110        assert_eq!(WeatherKind::Clear.parameters(40), (40, 0, false, false));
111        assert_eq!(WeatherKind::Rain.parameters(40), (0, 40, true, false));
112        assert_eq!(WeatherKind::Thunder.parameters(40), (0, 40, true, true));
113    }
114}