Skip to content

Commit 16f9469

Browse files
committed
Allow selecting options by line ID
1 parent f25cdd7 commit 16f9469

File tree

5 files changed

+117
-2
lines changed

5 files changed

+117
-2
lines changed

crates/bevy_plugin/assets/options.yarn

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ Ancient Reptilian Brain: Never ever.
1111
title: Hub0
1212
position: -43,-216
1313
---
14-
-> You: Never ever ever? <<if $never == false>>
14+
-> You: Never ever ever? <<if $never == false>> #line:x1
1515
Ancient Reptilian Brain: Never ever ever ever, baby!
1616
<<set $never to true>>
1717
<<jump Hub0>>
18-
-> You: (Simply keep on non-existing.)
18+
-> You: (Simply keep on non-existing.) #line:x2
1919
Ancient Reptilian Brain: An inordinate amount of time passes. It is utterly void of struggle. No ex-wives are contained within it.
2020

2121
<<set $great to false>>

crates/bevy_plugin/src/dialogue_runner.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ impl DialogueRunner {
102102
Ok(self)
103103
}
104104

105+
/// If the dialogue is currently waiting for the user to select an option, this method will select the option with the given id.
106+
/// Implies [`DialogueRunner::continue_in_next_update`].
107+
pub fn select_option_by_line_id(&mut self, line: LineId) -> Result<&mut Self> {
108+
if !self.is_running {
109+
bail!(
110+
"Can't select option of line id {line}: the dialogue is currently not running. Please call `DialogueRunner::continue_in_next_update()` only after receiving a `PresentOptionsEvent`."
111+
)
112+
}
113+
114+
let option = self
115+
.inner_mut()
116+
.0
117+
.set_selected_option_by_line_id(line)
118+
.map_err(Error::from)?;
119+
self.last_selected_option.replace(option);
120+
self.continue_in_next_update();
121+
Ok(self)
122+
}
123+
105124
/// Returns whether the dialogue runner is currently running. Returns `false` if:
106125
/// - The dialogue has not yet been started via [`DialogueRunner::start_node`]
107126
/// - The dialogue has been stopped via [`DialogueRunner::stop`]

crates/bevy_plugin/tests/test_dialogue_runner_delivers_options.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ fn errs_on_unexpected_selection_value() -> Result<()> {
5757
Ok(())
5858
}
5959

60+
#[test]
61+
fn errs_on_unexpected_line_id() -> Result<()> {
62+
let mut app = App::new();
63+
app.setup_dialogue_runner().start_node("Start");
64+
app.continue_dialogue_and_update_n_times(4);
65+
app.dialogue_runner_mut()
66+
.select_option_by_line_id(LineId("line:1".to_string()))
67+
.unwrap_err();
68+
69+
Ok(())
70+
}
71+
6072
#[test]
6173
fn option_selection_implies_continue() -> Result<()> {
6274
let mut app = App::new();
@@ -158,6 +170,35 @@ fn can_select_unavailable_choice() -> Result<()> {
158170
Ok(())
159171
}
160172

173+
#[test]
174+
fn can_select_by_line_id() -> Result<()> {
175+
let mut app = App::new();
176+
let mut asserter = EventAsserter::new();
177+
app.setup_dialogue_runner().start_node("Start");
178+
app.continue_dialogue_and_update_n_times(4);
179+
app.dialogue_runner_mut()
180+
.select_option_by_line_id(LineId("line:x1".to_string()))?;
181+
app.continue_dialogue_and_update();
182+
asserter.clear_events(&mut app);
183+
184+
app.continue_dialogue_and_update();
185+
assert_events!(asserter, app contains [
186+
PresentLineEvent (n = 0),
187+
PresentOptionsEvent with |event|
188+
event.options.len() == 2
189+
&& event.options.iter().filter(|o| o.is_available).count() == 1
190+
&& event.options.iter().filter(|o| !o.is_available).all(|o| o.id == OptionId(0)),
191+
]);
192+
app.dialogue_runner_mut().select_option(OptionId(0))?;
193+
app.update();
194+
assert_events!(asserter, app contains [
195+
PresentLineEvent with |event| event.line.text == lines()[6],
196+
PresentOptionsEvent (n = 0),
197+
]);
198+
199+
Ok(())
200+
}
201+
161202
#[test]
162203
fn generates_files_in_dev_mode() -> Result<()> {
163204
let dir = tempdir()?;

crates/runtime/src/dialogue.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ pub enum DialogueError {
3434
selected_option_id: OptionId,
3535
max_id: usize,
3636
},
37+
InvalidLineIdError {
38+
selected_line_id: LineId,
39+
line_ids: Vec<LineId>,
40+
},
3741
UnexpectedOptionSelectionError,
3842
ContinueOnOptionSelectionError,
3943
NoNodeSelectedOnContinue,
@@ -66,6 +70,10 @@ impl Display for DialogueError {
6670
MarkupParseError(e) => Display::fmt(e, f),
6771
LineProviderError { id, language_code } => write!(f, "Line ID \"{id}\" not found in line provider with language code {language_code:?}"),
6872
InvalidOptionIdError { selected_option_id, max_id } => write!(f, "{selected_option_id:?} is not a valid option ID (expected a number between 0 and {max_id}."),
73+
InvalidLineIdError { selected_line_id, line_ids } => {
74+
let line_ids = line_ids.iter().map(|id| id.0.clone()).collect::<Vec<_>>().join(", ");
75+
write!(f, "{selected_line_id:?} is not a valid line ID of options (expected line ids: {line_ids}.")
76+
},
6977
UnexpectedOptionSelectionError => f.write_str("An option was selected, but the dialogue wasn't waiting for a selection. This method should only be called after the Dialogue is waiting for the user to select an option."),
7078
ContinueOnOptionSelectionError => f.write_str("Dialogue was asked to continue running, but it is waiting for the user to select an option first."),
7179
NoNodeSelectedOnContinue => f.write_str("Cannot continue running dialogue. No node has been selected."),
@@ -475,6 +483,23 @@ impl Dialogue {
475483
Ok(self)
476484
}
477485

486+
/// Signals to the [`Dialogue`] that the user has selected a specified [`DialogueOption`].
487+
///
488+
/// This makes dialogue replay more robust than [`self.set_selected_option`] when adding new options.
489+
///
490+
/// The ID number that should be passed as the parameter to this method should be the id
491+
/// of the [`line`] field in the [`DialogueOption`] that represents the user's selection.
492+
///
493+
/// ## Panics
494+
/// - If the Dialogue is not expecting an option to be selected.
495+
/// - If the line ID is not found in the vector of [`DialogueOption`] provided by [`DialogueEvent::Options`].
496+
///
497+
/// ## See Also
498+
/// - [`Dialogue::continue_`]
499+
pub fn set_selected_option_by_line_id(&mut self, selected_line_id: LineId) -> Result<OptionId> {
500+
self.vm.set_selected_option_by_line_id(selected_line_id)
501+
}
502+
478503
/// Gets a value indicating whether the Dialogue is currently executing Yarn instructions.
479504
#[must_use]
480505
pub fn is_active(&self) -> bool {

crates/runtime/src/virtual_machine.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,36 @@ impl VirtualMachine {
254254
Ok(())
255255
}
256256

257+
pub(crate) fn set_selected_option_by_line_id(
258+
&mut self,
259+
selected_line_id: LineId,
260+
) -> Result<OptionId> {
261+
if self.execution_state != ExecutionState::WaitingOnOptionSelection {
262+
return Err(DialogueError::UnexpectedOptionSelectionError);
263+
}
264+
if let Some(selected_option) = self
265+
.state
266+
.current_options
267+
.iter()
268+
.find(|o| o.line.id == selected_line_id)
269+
{
270+
let selected_option_id = selected_option.id.clone();
271+
self.set_selected_option(selected_option_id)
272+
.map(|_| selected_option_id)
273+
} else {
274+
let line_ids = self
275+
.state
276+
.current_options
277+
.iter()
278+
.map(|o| o.line.id.clone())
279+
.collect();
280+
Err(DialogueError::InvalidLineIdError {
281+
selected_line_id,
282+
line_ids,
283+
})
284+
}
285+
}
286+
257287
pub(crate) fn is_active(&self) -> bool {
258288
self.execution_state != ExecutionState::Stopped
259289
}

0 commit comments

Comments
 (0)