Skip to main content

steel_core/command/builtins/
fly.rs

1//! Steel player-flight command.
2
3use std::{slice, sync::Arc};
4
5use steel_utils::{Identifier, translations};
6use text_components::TextComponent;
7
8use super::super::{
9    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
10    execution::{
11        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
12        literal,
13    },
14    registration::CommandRegistration,
15};
16use crate::player::{Abilities, DEFAULT_FLYING_SPEED, Player};
17
18const MAX_FLY_SPEED_MULTIPLIER: f32 = 30.0;
19
20pub(super) fn registration() -> CommandRegistration<CommandSource> {
21    CommandRegistration::new(Identifier::from_steel("fly"), |_| command())
22}
23
24fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
25    literal("fly")
26        .executes(toggle_sender_flight)
27        .then(
28            literal("target").then(
29                argument("targets", SteelArgumentType::players())
30                    .executes(toggle_target_flight)
31                    .then(argument("value", ArgumentType::bool()).executes(set_target_flight))
32                    .then(
33                        literal("speed")
34                            .executes(query_target_flying_speed)
35                            .then(speed_argument().executes(set_target_flying_speed)),
36                    ),
37            ),
38        )
39        .then(
40            literal("speed")
41                .executes(query_sender_flying_speed)
42                .then(speed_argument().executes(set_sender_flying_speed)),
43        )
44}
45
46fn speed_argument() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
47    argument("speed", ArgumentType::float(0.0, MAX_FLY_SPEED_MULTIPLIER))
48}
49
50fn toggle_sender_flight(
51    context: &SteelCommandContext<CommandSource>,
52) -> Result<i32, CommandSyntaxError> {
53    let player = source_player(context)?;
54    toggle_flight(slice::from_ref(player));
55    Ok(1)
56}
57
58fn toggle_target_flight(
59    context: &SteelCommandContext<CommandSource>,
60) -> Result<i32, CommandSyntaxError> {
61    let targets = context.players("targets")?;
62    toggle_flight(&targets);
63    Ok(1)
64}
65
66fn set_target_flight(
67    context: &SteelCommandContext<CommandSource>,
68) -> Result<i32, CommandSyntaxError> {
69    let targets = context.players("targets")?;
70    let allowed = context.boolean("value")?;
71    set_flight(&targets, allowed);
72    Ok(1)
73}
74
75fn query_target_flying_speed(
76    context: &SteelCommandContext<CommandSource>,
77) -> Result<i32, CommandSyntaxError> {
78    let targets = context.players("targets")?;
79    query_flying_speed(context.source(), &targets);
80    Ok(1)
81}
82
83fn set_target_flying_speed(
84    context: &SteelCommandContext<CommandSource>,
85) -> Result<i32, CommandSyntaxError> {
86    let targets = context.players("targets")?;
87    let multiplier = required_speed(context)?;
88    set_flying_speed(context.source(), &targets, multiplier);
89    Ok(1)
90}
91
92fn query_sender_flying_speed(
93    context: &SteelCommandContext<CommandSource>,
94) -> Result<i32, CommandSyntaxError> {
95    let player = source_player(context)?;
96    query_flying_speed(context.source(), slice::from_ref(player));
97    Ok(1)
98}
99
100fn set_sender_flying_speed(
101    context: &SteelCommandContext<CommandSource>,
102) -> Result<i32, CommandSyntaxError> {
103    let player = source_player(context)?;
104    let multiplier = required_speed(context)?;
105    set_flying_speed(context.source(), slice::from_ref(player), multiplier);
106    Ok(1)
107}
108
109fn source_player(
110    context: &SteelCommandContext<CommandSource>,
111) -> Result<&Arc<Player>, CommandSyntaxError> {
112    context.source().player().ok_or_else(|| {
113        CommandSyntaxError::dynamic(TextComponent::from(
114            &translations::PERMISSIONS_REQUIRES_PLAYER,
115        ))
116    })
117}
118
119fn required_speed(context: &SteelCommandContext<CommandSource>) -> Result<f32, CommandSyntaxError> {
120    context.float("speed")
121}
122
123fn toggle_flight(targets: &[Arc<Player>]) {
124    for target in targets {
125        {
126            let mut abilities = target.abilities.lock();
127            let allowed = !abilities.may_fly;
128            set_flight_allowed(&mut abilities, allowed);
129        }
130        target.send_abilities();
131    }
132}
133
134fn set_flight(targets: &[Arc<Player>], allowed: bool) {
135    for target in targets {
136        {
137            let mut abilities = target.abilities.lock();
138            set_flight_allowed(&mut abilities, allowed);
139        }
140        target.send_abilities();
141    }
142}
143
144const fn set_flight_allowed(abilities: &mut Abilities, allowed: bool) {
145    abilities.may_fly = allowed;
146    if !allowed {
147        abilities.flying = false;
148    }
149}
150
151fn set_flying_speed(source: &CommandSource, targets: &[Arc<Player>], multiplier: f32) {
152    let speed = speed_from_multiplier(multiplier);
153    for target in targets {
154        target.set_flying_speed(speed);
155        target.send_abilities();
156        source.send_success(
157            &TextComponent::plain(format!(
158                "Set flying speed for player '{}' to {multiplier:.1}x ({speed:.3})",
159                target.gameprofile.name
160            )),
161            true,
162        );
163    }
164}
165
166fn query_flying_speed(source: &CommandSource, targets: &[Arc<Player>]) {
167    for target in targets {
168        let speed = target.get_flying_speed();
169        let multiplier = speed / DEFAULT_FLYING_SPEED;
170        source.send_success(
171            &TextComponent::plain(format!(
172                "Current flying speed for player '{}': {multiplier:.1}x ({speed:.3})",
173                target.gameprofile.name
174            )),
175            false,
176        );
177    }
178}
179
180fn speed_from_multiplier(multiplier: f32) -> f32 {
181    multiplier * DEFAULT_FLYING_SPEED
182}
183
184#[cfg(test)]
185mod tests {
186    use super::super::create_dispatcher;
187    use super::{MAX_FLY_SPEED_MULTIPLIER, set_flight_allowed, speed_from_multiplier};
188    use crate::{
189        command::{
190            brigadier::{ArgumentType, CommandDispatcher, NodeId},
191            execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
192        },
193        player::{Abilities, DEFAULT_FLYING_SPEED},
194    };
195    use steel_registry::init_vanilla_registry;
196
197    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
198
199    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
200        let Some(children) = dispatcher.children(parent) else {
201            panic!("parent node should exist");
202        };
203        let Some(child) = children.iter().copied().find(|child| {
204            dispatcher
205                .node(*child)
206                .is_some_and(|node| node.name() == name)
207        }) else {
208            panic!("child {name} should exist");
209        };
210        child
211    }
212
213    #[test]
214    fn fly_graph_uses_explicit_target_and_bounded_speed_branches() {
215        init_vanilla_registry();
216        let Ok(dispatcher) = create_dispatcher() else {
217            panic!("built-in commands should register");
218        };
219        let fly = child(&dispatcher, dispatcher.root(), "fly");
220        assert!(matches!(
221            dispatcher.node(fly),
222            Some(node) if node.is_executable()
223        ));
224
225        let target = child(&dispatcher, fly, "target");
226        let targets = child(&dispatcher, target, "targets");
227        assert!(matches!(
228            dispatcher.node(targets),
229            Some(node)
230                if node.is_executable()
231                    && node.argument_type() == Some(&SteelArgumentType::players())
232        ));
233        let value = child(&dispatcher, targets, "value");
234        assert_eq!(
235            dispatcher.node(value).and_then(|node| node.argument_type()),
236            Some(&SteelArgumentType::from(ArgumentType::bool()))
237        );
238
239        let own_speed = child(&dispatcher, fly, "speed");
240        let own_speed_value = child(&dispatcher, own_speed, "speed");
241        let target_speed = child(&dispatcher, targets, "speed");
242        let target_speed_value = child(&dispatcher, target_speed, "speed");
243        let expected = SteelArgumentType::from(ArgumentType::float(0.0, MAX_FLY_SPEED_MULTIPLIER));
244        for node in [own_speed_value, target_speed_value] {
245            assert_eq!(
246                dispatcher.node(node).and_then(|node| node.argument_type()),
247                Some(&expected)
248            );
249            assert!(matches!(
250                dispatcher.node(node),
251                Some(node) if node.is_executable()
252            ));
253        }
254    }
255
256    #[test]
257    fn disabling_flight_clears_active_flying_state() {
258        let mut abilities = Abilities {
259            may_fly: true,
260            flying: true,
261            ..Abilities::default()
262        };
263
264        set_flight_allowed(&mut abilities, false);
265        assert!(!abilities.may_fly);
266        assert!(!abilities.flying);
267
268        set_flight_allowed(&mut abilities, true);
269        assert!(abilities.may_fly);
270        assert!(!abilities.flying);
271    }
272
273    #[test]
274    fn fly_speed_uses_vanilla_default_as_the_multiplier_base() {
275        let speed = speed_from_multiplier(MAX_FLY_SPEED_MULTIPLIER);
276        let expected = 30.0 * DEFAULT_FLYING_SPEED;
277        assert!((speed - expected).abs() <= f32::EPSILON);
278    }
279}