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.state.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: Actor) -> Option<Vec3<f32>> {
9    match actor {
10        Actor::Npc(npc_id) => ctx.state.data().npcs.get(npc_id).map(|npc| npc.wpos),
11        Actor::Character(character_id) => ctx
12            .system_data
13            .id_maps
14            .character_entity(character_id)
15            .and_then(|c| ctx.system_data.positions.get(c))
16            .map(|p| p.0),
17    }
18}
19
20pub fn actor_exists(ctx: &NpcCtx, actor: Actor) -> bool {
21    match actor {
22        Actor::Npc(npc_id) => ctx.state.data().npcs.contains_key(npc_id),
23        Actor::Character(character_id) => ctx
24            .system_data
25            .id_maps
26            .character_entity(character_id)
27            .is_some(),
28    }
29}
30
31pub fn talk<S: State>(tgt: Actor) -> impl Action<S> + Clone {
32    just(move |ctx, _| ctx.controller.do_talk(tgt)).debug(|| "talking")
33}
34
35pub fn do_dialogue<S: State, T: Default + Clone + Send + Sync + 'static, A: Action<S, T>>(
36    tgt: Actor,
37    f: impl Fn(DialogueSession) -> A + Send + Sync + 'static,
38) -> impl Action<S, T> {
39    now(move |ctx, _| {
40        let session = ctx.controller.dialogue_start(tgt);
41        f(session)
42            // If an end dialogue message is received, stop the dialogue
43            .stop_if(move |ctx: &mut NpcCtx| {
44                let mut stop = false;
45                ctx.inbox.retain(|input| {
46                    if let NpcInput::Dialogue(_, dialogue) = input
47                        && dialogue.id == session.id
48                        && let DialogueKind::End = dialogue.kind
49                    {
50                        stop = true;
51                        false
52                    } else {
53                        true
54                    }
55                });
56                stop
57            })
58            .and_then(move |x: Option<T>| just(move |ctx, _| {
59                ctx.controller.do_idle();
60                ctx.controller.dialogue_end(session);
61                x.clone().unwrap_or_default()
62            }))
63    })
64}
65
66impl DialogueSession {
67    /// Ask a question as part of a dialogue.
68    ///
69    /// Responses will be verified against the original response options and
70    /// dialogue participant to prevent spoofing.
71    pub fn ask_question<
72        S: State,
73        R: Into<Response>,
74        T: Default + Send + Sync + 'static,
75        A: Action<S, T>,
76    >(
77        self,
78        question: Content,
79        responses: impl IntoIterator<Item = (R, A)> + Send + Sync + 'static,
80    ) -> impl Action<S, T> {
81        let (responses, actions): (Vec<_>, Vec<_>) = responses
82            .into_iter()
83            .enumerate()
84            .map(|(idx, (r, a))| ((idx as u16, r.into()), a))
85            .unzip();
86
87        let actions_once = take_once::TakeOnce::new();
88        let _ = actions_once.store(actions);
89
90        now(move |ctx, _| {
91            let q_tag = ctx.controller.dialogue_question(
92                self,
93                question.clone(),
94                responses.iter().cloned(),
95            );
96            let responses = responses.clone();
97            until(move |ctx, _| {
98                let mut id = None;
99                ctx.inbox.retain(|input| {
100                    if let NpcInput::Dialogue(_, dialogue) = input
101                        // Check that the response is for the same dialogue
102                        && dialogue.id == self.id
103                        && let DialogueKind::Response { tag, response_id, response, .. } = &dialogue.kind
104                        // Check that the response relates the the question just asked
105                        && *tag == q_tag
106                        // Check that the response matches one of our requested responses
107                        && responses.iter().any(|(r_id, r)| r_id == response_id && r == response)
108                    {
109                        id = Some(*response_id);
110                        false
111                    } else {
112                        true
113                    }
114                });
115                match id {
116                    // TODO: Should be 'engage target in conversation'
117                    None => ControlFlow::Continue(talk(self.target)),
118                    Some(response_id) => ControlFlow::Break(response_id),
119                }
120            })
121        })
122            // Add some thinking time after hearing a response
123            .and_then(move |response_id| talk(self.target).repeat().stop_if(timeout(0.5)).map(move |_, _| response_id))
124            // If all else fails, add a timeout to dialogues
125            // TODO: Only timeout if no messages have been received recently
126            .stop_if(timeout(60.0))
127            .and_then(move |resp: Option<u16>| {
128                if let Some(action) = resp.and_then(|resp| actions_once.take().unwrap().into_iter().nth(resp as usize)) {
129                    action.map(|x, _| x).boxed()
130                } else {
131                    idle().map(|_, _| Default::default()).boxed()
132                }
133            })
134    }
135
136    pub fn say_statement<S: State>(self, stmt: Content) -> impl Action<S> {
137        now(move |ctx, _| {
138            ctx.controller.dialogue_statement(self, stmt.clone());
139            idle()
140        })
141        .then(talk(self.target)
142            .repeat()
143            // Wait for a while before making the statement to allow other dialogue to be read
144            .stop_if(timeout(2.5)))
145        .map(|_, _| ())
146    }
147}