1use crate::{
7 character::CharacterId,
8 comp::{agent::FlightMode, dialogue::Subject, inventory::item::ItemDef},
9 util::Dir,
10};
11use common_i18n::Content;
12use rand::{Rng, seq::IteratorRandom};
13use serde::{Deserialize, Serialize};
14use specs::Component;
15use std::{collections::VecDeque, sync::Arc};
16use strum::{EnumIter, IntoEnumIterator};
17use vek::*;
18
19slotmap::new_key_type! { pub struct NpcId; }
20
21slotmap::new_key_type! { pub struct SiteId; }
22
23slotmap::new_key_type! { pub struct FactionId; }
24
25slotmap::new_key_type! { pub struct ReportId; }
26
27slotmap::new_key_type! { pub struct AirshipRouteId; }
28
29#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
30pub struct RtSimEntity(pub NpcId);
31
32impl Component for RtSimEntity {
33 type Storage = specs::VecStorage<Self>;
34}
35
36#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
37pub enum Actor {
38 Npc(NpcId),
39 Character(CharacterId),
40}
41
42impl Actor {
43 pub fn npc(&self) -> Option<NpcId> {
44 match self {
45 Actor::Npc(id) => Some(*id),
46 Actor::Character(_) => None,
47 }
48 }
49}
50
51impl From<NpcId> for Actor {
52 fn from(value: NpcId) -> Self { Actor::Npc(value) }
53}
54
55impl From<CharacterId> for Actor {
56 fn from(value: CharacterId) -> Self { Actor::Character(value) }
57}
58
59#[derive(EnumIter, Clone, Copy)]
60pub enum PersonalityTrait {
61 Open,
62 Adventurous,
63 Closed,
64 Conscientious,
65 Busybody,
66 Unconscientious,
67 Extroverted,
68 Introverted,
69 Agreeable,
70 Sociable,
71 Disagreeable,
72 Neurotic,
73 Seeker,
74 Worried,
75 SadLoner,
76 Stable,
77}
78
79#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
80pub struct Personality {
81 openness: u8,
82 conscientiousness: u8,
83 extraversion: u8,
84 agreeableness: u8,
85 neuroticism: u8,
86}
87
88fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 {
89 let l = max - min;
90 min + rng.gen_range(0..=l / 3)
91 + rng.gen_range(0..=l / 3 + l % 3 % 2)
92 + rng.gen_range(0..=l / 3 + l % 3 / 2)
93}
94
95impl Personality {
96 pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
97 pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
98 pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
99 pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
100 const MAX: u8 = 255;
101 pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
102 const MIN: u8 = 0;
103
104 fn distributed_value(rng: &mut impl Rng) -> u8 { distributed(Self::MIN, Self::MAX, rng) }
105
106 pub fn random(rng: &mut impl Rng) -> Self {
107 Self {
108 openness: Self::distributed_value(rng),
109 conscientiousness: Self::distributed_value(rng),
110 extraversion: Self::distributed_value(rng),
111 agreeableness: Self::distributed_value(rng),
112 neuroticism: Self::distributed_value(rng),
113 }
114 }
115
116 pub fn random_evil(rng: &mut impl Rng) -> Self {
117 Self {
118 openness: Self::distributed_value(rng),
119 extraversion: Self::distributed_value(rng),
120 neuroticism: Self::distributed_value(rng),
121 agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
122 conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
123 }
124 }
125
126 pub fn random_good(rng: &mut impl Rng) -> Self {
127 Self {
128 openness: Self::distributed_value(rng),
129 extraversion: Self::distributed_value(rng),
130 neuroticism: Self::distributed_value(rng),
131 agreeableness: Self::distributed_value(rng),
132 conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
133 }
134 }
135
136 pub fn is(&self, trait_: PersonalityTrait) -> bool {
137 match trait_ {
138 PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
139 PersonalityTrait::Adventurous => {
140 self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID
141 },
142 PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
143 PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
144 PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
145 PersonalityTrait::Unconscientious => {
146 self.conscientiousness < Personality::LOW_THRESHOLD
147 },
148 PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
149 PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
150 PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
151 PersonalityTrait::Sociable => {
152 self.agreeableness > Personality::HIGH_THRESHOLD
153 && self.extraversion > Personality::MID
154 },
155 PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
156 PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
157 PersonalityTrait::Seeker => {
158 self.neuroticism > Personality::HIGH_THRESHOLD
159 && self.openness > Personality::LITTLE_HIGH
160 },
161 PersonalityTrait::Worried => {
162 self.neuroticism > Personality::HIGH_THRESHOLD
163 && self.agreeableness > Personality::LITTLE_HIGH
164 },
165 PersonalityTrait::SadLoner => {
166 self.neuroticism > Personality::HIGH_THRESHOLD
167 && self.extraversion < Personality::LITTLE_LOW
168 },
169 PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
170 }
171 }
172
173 pub fn chat_trait(&self, rng: &mut impl Rng) -> Option<PersonalityTrait> {
174 PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
175 }
176
177 pub fn will_ambush(&self) -> bool {
178 self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
179 }
180
181 pub fn get_generic_comment(&self, rng: &mut impl Rng) -> Content {
182 let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
183 match extreme_trait {
184 PersonalityTrait::Open => "npc-speech-villager_open",
185 PersonalityTrait::Adventurous => "npc-speech-villager_adventurous",
186 PersonalityTrait::Closed => "npc-speech-villager_closed",
187 PersonalityTrait::Conscientious => "npc-speech-villager_conscientious",
188 PersonalityTrait::Busybody => "npc-speech-villager_busybody",
189 PersonalityTrait::Unconscientious => "npc-speech-villager_unconscientious",
190 PersonalityTrait::Extroverted => "npc-speech-villager_extroverted",
191 PersonalityTrait::Introverted => "npc-speech-villager_introverted",
192 PersonalityTrait::Agreeable => "npc-speech-villager_agreeable",
193 PersonalityTrait::Sociable => "npc-speech-villager_sociable",
194 PersonalityTrait::Disagreeable => "npc-speech-villager_disagreeable",
195 PersonalityTrait::Neurotic => "npc-speech-villager_neurotic",
196 PersonalityTrait::Seeker => "npc-speech-villager_seeker",
197 PersonalityTrait::SadLoner => "npc-speech-villager_sad_loner",
198 PersonalityTrait::Worried => "npc-speech-villager_worried",
199 PersonalityTrait::Stable => "npc-speech-villager_stable",
200 }
201 } else {
202 "npc-speech-villager"
203 };
204
205 Content::localized(i18n_key)
206 }
207}
208
209impl Default for Personality {
210 fn default() -> Self {
211 Self {
212 openness: Personality::MID,
213 conscientiousness: Personality::MID,
214 extraversion: Personality::MID,
215 agreeableness: Personality::MID,
216 neuroticism: Personality::MID,
217 }
218 }
219}
220
221#[derive(Clone, Debug, Default)]
231pub struct RtSimController {
232 pub activity: Option<NpcActivity>,
233 pub actions: VecDeque<NpcAction>,
234 pub personality: Personality,
235 pub heading_to: Option<String>,
236 pub look_dir: Option<Dir>,
238}
239
240impl RtSimController {
241 pub fn with_destination(pos: Vec3<f32>) -> Self {
242 Self {
243 activity: Some(NpcActivity::Goto(pos, 0.5)),
244 ..Default::default()
245 }
246 }
247}
248
249#[derive(Clone, Copy, Debug)]
250pub enum NpcActivity {
251 Goto(Vec3<f32>, f32),
253 GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
256 Gather(&'static [ChunkResource]),
257 HuntAnimals,
259 Dance(Option<Dir>),
260 Cheer(Option<Dir>),
261 Sit(Option<Dir>, Option<Vec3<i32>>),
262 Talk(Actor),
263}
264
265#[derive(Clone, Debug)]
268pub enum NpcAction {
269 Say(Option<Actor>, Content),
273 Attack(Actor),
275 Dialogue(Actor, Dialogue),
276}
277
278#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
279pub struct DialogueId(pub u64);
280
281#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
285pub struct Dialogue<const IS_VALIDATED: bool = false> {
286 pub id: DialogueId,
287 pub kind: DialogueKind,
288}
289impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
290
291impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
292 pub fn message(&self) -> Option<&Content> {
293 match &self.kind {
294 DialogueKind::Start | DialogueKind::End => None,
295 DialogueKind::Statement(msg) | DialogueKind::Question { msg, .. } => Some(msg),
296 DialogueKind::Response { response, .. } => Some(&response.msg),
297 }
298 }
299}
300
301impl Dialogue<false> {
302 pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
303}
304
305#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
306pub enum DialogueKind {
307 Start,
308 End,
309 Statement(Content),
310 Question {
311 tag: u32,
313 msg: Content,
314 responses: Vec<(u16, Response)>,
316 },
317 Response {
318 tag: u32,
320 response: Response,
321 response_id: u16,
322 },
323}
324
325#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
326pub struct Response {
327 pub msg: Content,
328 pub given_item: Option<(Arc<ItemDef>, u32)>,
329}
330
331impl From<Content> for Response {
332 fn from(msg: Content) -> Self {
333 Self {
334 msg,
335 given_item: None,
336 }
337 }
338}
339
340#[derive(Clone, Debug)]
342pub enum NpcInput {
343 Report(ReportId),
344 Interaction(Actor, Subject),
345 Dialogue(Actor, Dialogue<true>),
346}
347
348#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
351pub enum ChunkResource {
352 #[serde(rename = "0")]
353 Grass,
354 #[serde(rename = "1")]
355 Flower,
356 #[serde(rename = "2")]
357 Fruit,
358 #[serde(rename = "3")]
359 Vegetable,
360 #[serde(rename = "4")]
361 Mushroom,
362 #[serde(rename = "5")]
363 Loot, #[serde(rename = "6")]
365 Plant, #[serde(rename = "7")]
367 Stone,
368 #[serde(rename = "8")]
369 Wood, #[serde(rename = "9")]
371 Gem, #[serde(rename = "a")]
373 Ore, }
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
379pub enum Role {
380 #[serde(rename = "0")]
381 Civilised(Option<Profession>),
382 #[serde(rename = "1")]
383 Wild,
384 #[serde(rename = "2")]
385 Monster,
386 #[serde(rename = "3")]
387 Vehicle,
388}
389
390#[derive(Clone, Debug, Serialize, Deserialize)]
393pub enum Profession {
394 #[serde(rename = "0")]
395 Farmer,
396 #[serde(rename = "1")]
397 Hunter,
398 #[serde(rename = "2")]
399 Merchant,
400 #[serde(rename = "3")]
401 Guard,
402 #[serde(rename = "4")]
403 Adventurer(u32),
404 #[serde(rename = "5")]
405 Blacksmith,
406 #[serde(rename = "6")]
407 Chef,
408 #[serde(rename = "7")]
409 Alchemist,
410 #[serde(rename = "8")]
411 Pirate,
412 #[serde(rename = "9")]
413 Cultist,
414 #[serde(rename = "10")]
415 Herbalist,
416 #[serde(rename = "11")]
417 Captain,
418}
419
420#[derive(Clone, Debug, Serialize, Deserialize)]
421pub struct WorldSettings {
422 pub start_time: f64,
423}
424
425impl Default for WorldSettings {
426 fn default() -> Self {
427 Self {
428 start_time: 9.0 * 3600.0, }
430 }
431}