veloren_rtsim/rule/npc_ai/
dialogue.rs1use super::*;
2
3pub fn general<S: State>(tgt: Actor, session: DialogueSession) -> impl Action<S> {
4 now(move |ctx, _| {
5 let can_be_hired = matches!(ctx.npc.profession(), Some(Profession::Adventurer(_)));
6 let is_hired_by_tgt = ctx.npc.hiring.is_some_and(|(a, _)| a == tgt);
7
8 let mut responses = Vec::new();
9
10 responses.push((
11 Response::from(Content::localized("dialogue-question-site")),
12 dialogue::about_site(session).boxed(),
13 ));
14 responses.push((
15 Response::from(Content::localized("dialogue-question-self")),
16 dialogue::about_self(session).boxed(),
17 ));
18 responses.push((
19 Response::from(Content::localized("dialogue-question-sentiment")),
20 dialogue::sentiments(tgt, session).boxed(),
21 ));
22 if is_hired_by_tgt {
23 responses.push((
24 Response::from(Content::localized("dialogue-cancel_hire")),
25 session
26 .say_statement(Content::localized("npc-dialogue-hire_cancelled"))
27 .then(just(move |ctx, _| ctx.controller.end_hiring()))
28 .boxed(),
29 ));
30 } else if can_be_hired {
31 responses.push((
32 Response::from(Content::localized("dialogue-question-hire")),
33 dialogue::hire(tgt, session).boxed(),
34 ));
35 }
36 responses.push((
37 Response::from(Content::localized("dialogue-question-directions")),
38 dialogue::directions(session).boxed(),
39 ));
40
41 session.ask_question(Content::localized("npc-question-general"), responses)
42 })
43}
44
45fn about_site<S: State>(session: DialogueSession) -> impl Action<S> {
46 now(move |ctx, _| {
47 if let Some(site_name) = util::site_name(ctx, ctx.npc.current_site) {
48 let mut action = session
49 .say_statement(Content::localized_with_args("npc-info-current_site", [(
50 "site",
51 Content::Plain(site_name),
52 )]))
53 .boxed();
54
55 if let Some(current_site) = ctx.npc.current_site
56 && let Some(current_site) = ctx.state.data().sites.get(current_site)
57 {
58 for mention_site in ¤t_site.nearby_sites_by_size {
59 if ctx.rng.gen_bool(0.5)
60 && let Some(content) = tell_site_content(ctx, *mention_site)
61 {
62 action = action.then(session.say_statement(content)).boxed();
63 }
64 }
65 }
66
67 action
68 } else {
69 session
70 .say_statement(Content::localized("npc-info-unknown"))
71 .boxed()
72 }
73 })
74}
75
76fn about_self<S: State>(session: DialogueSession) -> impl Action<S> {
77 now(move |ctx, _| {
78 let name = Content::localized_with_args("npc-info-self_name", [(
79 "name",
80 Content::Plain(ctx.npc.get_name()),
81 )]);
82
83 let job = ctx
84 .npc
85 .profession()
86 .map(|p| match p {
87 Profession::Farmer => "npc-info-role_farmer",
88 Profession::Hunter => "npc-info-role_hunter",
89 Profession::Merchant => "npc-info-role_merchant",
90 Profession::Guard => "npc-info-role_guard",
91 Profession::Adventurer(_) => "npc-info-role_adventurer",
92 Profession::Blacksmith => "npc-info-role_blacksmith",
93 Profession::Chef => "npc-info-role_chef",
94 Profession::Alchemist => "npc-info-role_alchemist",
95 Profession::Pirate => "npc-info-role_pirate",
96 Profession::Cultist => "npc-info-role_cultist",
97 Profession::Herbalist => "npc-info-role_herbalist",
98 Profession::Captain => "npc-info-role_captain",
99 })
100 .map(|p| {
101 Content::localized_with_args("npc-info-role", [("role", Content::localized(p))])
102 })
103 .unwrap_or_else(|| Content::localized("npc-info-role_none"));
104
105 let home = if let Some(site_name) = util::site_name(ctx, ctx.npc.home) {
106 Content::localized_with_args("npc-info-self_home", [(
107 "site",
108 Content::Plain(site_name),
109 )])
110 } else {
111 Content::localized("npc-info-self_homeless")
112 };
113
114 session
115 .say_statement(name)
116 .then(session.say_statement(job))
117 .then(session.say_statement(home))
118 })
119}
120
121fn sentiments<S: State>(tgt: Actor, session: DialogueSession) -> impl Action<S> {
122 session.ask_question(Content::Plain("...".to_string()), [(
123 Content::localized("dialogue-me"),
124 now(move |ctx, _| {
125 if ctx.sentiments.toward(tgt).is(Sentiment::ALLY) {
126 session.say_statement(Content::localized("npc-response-like_you"))
127 } else if ctx.sentiments.toward(tgt).is(Sentiment::RIVAL) {
128 session.say_statement(Content::localized("npc-response-dislike_you"))
129 } else {
130 session.say_statement(Content::localized("npc-response-ambivalent_you"))
131 }
132 }),
133 )])
134}
135
136fn hire<S: State>(tgt: Actor, session: DialogueSession) -> impl Action<S> {
137 now(move |ctx, _| {
138 if ctx.npc.hiring.is_none() && ctx.npc.rng(38792).gen_bool(0.5) {
139 let hire_level = match ctx.npc.profession() {
140 Some(Profession::Adventurer(l)) => l,
141 _ => 0,
142 };
143 let price_mul = 1u32 << hire_level.min(31);
144 let mut responses = Vec::new();
145 responses.push((
146 Response::from(Content::localized("dialogue-cancel_interaction")),
147 session
148 .say_statement(Content::localized("npc-response-no_problem"))
149 .boxed(),
150 ));
151 let options = [
152 (
153 1.0,
154 60,
155 Content::localized_attr("dialogue-buy_hire_days", "day"),
156 ),
157 (
158 7.0,
159 300,
160 Content::localized_attr("dialogue-buy_hire_days", "week"),
161 ),
162 ];
163 for (days, base_price, msg) in options {
164 responses.push((
165 Response {
166 msg,
167 given_item: Some((
168 Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
169 price_mul.saturating_mul(base_price),
170 )),
171 },
172 session
173 .say_statement(Content::localized("npc-response-accept_hire"))
174 .then(just(move |ctx, _| {
175 ctx.controller.set_newly_hired(
176 tgt,
177 ctx.time.add_days(days, &ctx.system_data.server_constants),
178 );
179 }))
180 .boxed(),
181 ));
182 }
183 session
184 .ask_question(Content::localized("npc-response-hire_time"), responses)
185 .boxed()
186 } else {
187 session
188 .say_statement(Content::localized("npc-response-decline_hire"))
189 .boxed()
190 }
191 })
192}
193
194fn directions<S: State>(session: DialogueSession) -> impl Action<S> {
195 now(move |ctx, _| {
196 let mut responses = Vec::new();
197
198 if let Some(current_site) = ctx.npc.current_site
199 && let Some(ws_id) = ctx.state.data().sites[current_site].world_site
200 {
201 let direction_to_nearest =
202 |f: fn(&&world::site2::Plot) -> bool,
203 plot_name: fn(&world::site2::Plot) -> Content| {
204 now(move |ctx, _| {
205 if let Some(ws) = ctx.index.sites.get(ws_id).site2() {
206 if let Some(p) = ws.plots().filter(f).min_by_key(|p| {
207 ws.tile_center_wpos(p.root_tile())
208 .distance_squared(ctx.npc.wpos.xy().as_())
209 }) {
210 ctx.controller.dialogue_marker(
211 session,
212 ws.tile_center_wpos(p.root_tile()),
213 plot_name(p),
214 );
215 session.say_statement(Content::localized("npc-response-directions"))
216 } else {
217 session
218 .say_statement(Content::localized("npc-response-doesnt_exist"))
219 }
220 } else {
221 session.say_statement(Content::localized("npc-info-unknown"))
222 }
223 })
224 .boxed()
225 };
226
227 responses.push((
228 Content::localized("dialogue-direction-tavern"),
229 direction_to_nearest(
230 |p| matches!(p.kind(), PlotKind::Tavern(_)),
231 |p| match p.kind() {
232 PlotKind::Tavern(t) => Content::Plain(t.name.clone()),
233 _ => unreachable!(),
234 },
235 ),
236 ));
237 responses.push((
238 Content::localized("dialogue-direction-plaza"),
239 direction_to_nearest(
240 |p| matches!(p.kind(), PlotKind::Plaza(_)),
241 |_| Content::localized("hud-map-plaza"),
242 ),
243 ));
244 responses.push((
245 Content::localized("dialogue-direction-workshop"),
246 direction_to_nearest(
247 |p| matches!(p.kind().meta(), Some(PlotKindMeta::Workshop { .. })),
248 |_| Content::localized("hud-map-workshop"),
249 ),
250 ));
251 responses.push((
252 Content::localized("dialogue-direction-airship_dock"),
253 direction_to_nearest(
254 |p| matches!(p.kind().meta(), Some(PlotKindMeta::AirshipDock { .. })),
255 |_| Content::localized("hud-map-airship_dock"),
256 ),
257 ));
258 }
259
260 session.ask_question(Content::localized("npc-question-directions"), responses)
261 })
262}