Skip to main content

veloren_rtsim/rule/npc_ai/
util.rs

1use super::*;
2
3pub fn site_name(ctx: &NpcCtx, site_id: impl Into<Option<SiteId>>) -> Option<String> {
4    let world_site = ctx.data.sites.get(site_id.into()?)?.world_site?;
5    Some(ctx.index.sites.get(world_site).name()?.to_string())
6}
7
8pub fn locate_actor(ctx: &NpcCtx, actor: ActorId) -> Option<Vec3<f32>> {
9    ctx.data.actors.get(actor).map(|actor| actor.wpos)
10}
11
12pub fn actor_exists(ctx: &NpcCtx, actor: ActorId) -> bool { ctx.data.actors.contains_key(actor) }
13
14pub fn actor_name(ctx: &NpcCtx, actor: ActorId) -> Option<String> {
15    ctx.data
16        .actors
17        .get(actor)
18        .and_then(|actor| actor.get_name())
19}
20
21pub fn talk<S: State>(tgt: ActorId) -> impl Action<S> + Clone {
22    just(move |ctx, _| ctx.controller.do_talk(tgt)).debug(|| "talking")
23}
24
25pub fn do_dialogue<S: State, T: Default + Clone + Send + Sync + 'static, A: Action<S, T>>(
26    tgt: ActorId,
27    f: impl Fn(DialogueSession) -> A + Clone + Send + Sync + 'static,
28) -> impl Action<S, T> {
29    now(move |ctx, _| {
30        let session = ctx.controller.dialogue_start(tgt);
31        f(session)
32            // If an end dialogue message is received, stop the dialogue
33            .stop_if(move |ctx: &mut NpcCtx| {
34                let mut stop = false;
35                ctx.inbox.retain(|input| {
36                    if let NpcInput::Dialogue(_, dialogue) = input
37                        && dialogue.id == session.id
38                        && let DialogueKind::End = dialogue.kind
39                    {
40                        stop = true;
41                        false
42                    } else {
43                        true
44                    }
45                });
46                stop
47            })
48            // As a backstop, timeout after 10 minutes of dialogue to avoid accidentally soft-locking NPCs
49            .stop_if(timeout(600.0))
50            .and_then(move |x: Option<Option<T>>| just(move |ctx, _| {
51                ctx.controller.do_idle();
52                ctx.controller.dialogue_end(session);
53                x.clone().flatten().unwrap_or_default()
54            }))
55            // Handle early cancellation elegantly
56            .when_cancelled(move |ctx: &mut NpcCtx| ctx.controller.dialogue_end(session))
57    })
58    .with_important_priority()
59}
60
61impl DialogueSession {
62    /// Ask a question as part of a dialogue.
63    ///
64    /// Responses will be verified against the original response options and
65    /// dialogue participant to prevent spoofing.
66    pub fn ask_question<
67        S: State,
68        R: Into<Response>,
69        T: Default + Send + Sync + 'static,
70        A: Action<S, T>,
71    >(
72        self,
73        question: Content,
74        responses: impl IntoIterator<Item = (R, A)> + Send + Sync + 'static,
75    ) -> impl Action<S, T> {
76        let (responses, actions): (Vec<_>, Vec<_>) = responses
77            .into_iter()
78            .enumerate()
79            .map(|(idx, (r, a))| ((idx as u16, r.into()), a))
80            .unzip();
81
82        let actions_once = Arc::new(take_once::TakeOnce::new());
83        let _ = actions_once.store(actions);
84
85        now(move |ctx, _| {
86            let q_tag = ctx.controller.dialogue_question(
87                self,
88                question.clone(),
89                responses.iter().cloned(),
90            );
91            let responses = responses.clone();
92            until(move |ctx, _| {
93                let mut id = None;
94                ctx.inbox.retain(|input| {
95                    if let NpcInput::Dialogue(_, dialogue) = input
96                        // Check that the response is for the same dialogue
97                        && dialogue.id == self.id
98                        && let DialogueKind::Response { tag, response_id, response, .. } = &dialogue.kind
99                        // Check that the response relates to the question just asked
100                        && *tag == q_tag
101                        // Check that the response matches one of our requested responses
102                        && responses.iter().any(|(r_id, r)| r_id == response_id && r == response)
103                    {
104                        id = Some(*response_id);
105                        false
106                    } else {
107                        true
108                    }
109                });
110                match id {
111                    // TODO: Should be 'engage target in conversation'
112                    None => ControlFlow::Continue(talk(self.target)),
113                    Some(response_id) => ControlFlow::Break(response_id),
114                }
115            })
116        })
117            // Add some thinking time after hearing a response
118            .and_then(move |response_id| talk(self.target).repeat().stop_if(timeout(0.5)).map(move |_, _| response_id))
119            // If all else fails, add a timeout to dialogues
120            // TODO: Only timeout if no messages have been received recently
121            .stop_if(timeout(60.0))
122            .and_then(move |resp: Option<u16>| {
123                if let Some(action) = resp.and_then(|resp| actions_once.take().unwrap().into_iter().nth(resp as usize)) {
124                    action.map(|x, _| x).boxed()
125                } else {
126                    idle().map(|_, _| Default::default()).boxed()
127                }
128            })
129    }
130
131    pub fn ask_yes_no_question<S: State>(self, question: Content) -> impl Action<S, bool> {
132        let answer = |is_yes| just(move |_, _| is_yes);
133        self.ask_question(question, [
134            (Content::localized("common-yes"), answer(true)),
135            (Content::localized("common-no"), answer(false)),
136        ])
137    }
138
139    pub fn say_statement_with_gift<S: State>(
140        self,
141        stmt: Content,
142        item: Option<(Arc<ItemDef>, u32)>,
143    ) -> impl Action<S> {
144        now(move |ctx, _| {
145            let s_tag = ctx
146                .controller
147                .dialogue_statement(self, stmt.clone(), item.clone());
148            // Wait for the ack
149            talk(self.target)
150            .repeat()
151            // Wait for a while before accepting the ACK to avoid dialogue progressing too fast
152            .stop_if(timeout(1.5))
153            // Wait for the ACK
154            .then(until(move |ctx, _| {
155                    let mut acked = false;
156                    ctx.inbox.retain(|input| {
157                        if let NpcInput::Dialogue(_, dialogue) = input
158                            // Check that the response is for the same dialogue
159                            && dialogue.id == self.id
160                            && let DialogueKind::Ack { tag } = &dialogue.kind
161                            // Check that the ACL relates to the statement just given
162                            && *tag == s_tag
163                        {
164                            acked = true;
165                            false
166                        } else {
167                            true
168                        }
169                    });
170                    if acked {
171                       ControlFlow::Break(())
172                    } else {
173                        ControlFlow::Continue(talk(self.target))
174                    }
175                })
176                    // As a final safeguard, timeout after a while
177                    .stop_if(timeout(30.0)))
178        })
179        .map(|_, _| ())
180    }
181
182    pub fn say_statement<S: State>(self, stmt: Content) -> impl Action<S> {
183        self.say_statement_with_gift(stmt, None)
184    }
185
186    pub fn give_marker<S: State>(self, marker: Marker) -> impl Action<S> {
187        just(move |ctx, _| ctx.controller.dialogue_marker(self, marker.clone()))
188    }
189}