1use crate::{
17 assets::AssetExt,
18 comp::{agent::FlightMode, inventory::item::ItemDef},
19 map::Marker,
20 util::Dir,
21};
22use common_i18n::Content;
23use rand::{RngExt, seq::IteratorRandom};
24use serde::{Deserialize, Serialize};
25use specs::Component;
26use std::{collections::VecDeque, sync::Arc};
27use strum::{EnumIter, IntoEnumIterator};
28use vek::*;
29
30slotmap::new_key_type! {
31 pub struct ActorId;
32 pub struct SiteId;
33 pub struct FactionId;
34 pub struct ReportId;
35}
36
37#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
38pub struct QuestId(pub u64);
39
40impl Component for ActorId {
41 type Storage = specs::VecStorage<Self>;
42}
43
44#[derive(EnumIter, Clone, Copy)]
45pub enum PersonalityTrait {
46 Open,
47 Adventurous,
48 Closed,
49 Conscientious,
50 Busybody,
51 Unconscientious,
52 Extroverted,
53 Introverted,
54 Agreeable,
55 Sociable,
56 Disagreeable,
57 Neurotic,
58 Seeker,
59 Worried,
60 SadLoner,
61 Stable,
62}
63
64#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
65pub struct Personality {
66 openness: u8,
67 conscientiousness: u8,
68 extraversion: u8,
69 agreeableness: u8,
70 neuroticism: u8,
71}
72
73fn distributed(min: u8, max: u8, rng: &mut impl RngExt) -> u8 {
74 let l = max - min;
75 min + rng.random_range(0..=l / 3)
76 + rng.random_range(0..=l / 3 + l % 3 % 2)
77 + rng.random_range(0..=l / 3 + l % 3 / 2)
78}
79
80impl Personality {
81 pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
82 pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
83 pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
84 pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
85 const MAX: u8 = 255;
86 pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
87 const MIN: u8 = 0;
88
89 fn distributed_value(rng: &mut impl RngExt) -> u8 { distributed(Self::MIN, Self::MAX, rng) }
90
91 pub fn random(rng: &mut impl RngExt) -> Self {
92 Self {
93 openness: Self::distributed_value(rng),
94 conscientiousness: Self::distributed_value(rng),
95 extraversion: Self::distributed_value(rng),
96 agreeableness: Self::distributed_value(rng),
97 neuroticism: Self::distributed_value(rng),
98 }
99 }
100
101 pub fn random_evil(rng: &mut impl RngExt) -> Self {
102 Self {
103 openness: Self::distributed_value(rng),
104 extraversion: Self::distributed_value(rng),
105 neuroticism: Self::distributed_value(rng),
106 agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
107 conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
108 }
109 }
110
111 pub fn random_good(rng: &mut impl RngExt) -> Self {
112 Self {
113 openness: Self::distributed_value(rng),
114 extraversion: Self::distributed_value(rng),
115 neuroticism: Self::distributed_value(rng),
116 agreeableness: Self::distributed_value(rng),
117 conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
118 }
119 }
120
121 pub fn is(&self, trait_: PersonalityTrait) -> bool {
122 match trait_ {
123 PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
124 PersonalityTrait::Adventurous => {
125 self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID
126 },
127 PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
128 PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
129 PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
130 PersonalityTrait::Unconscientious => {
131 self.conscientiousness < Personality::LOW_THRESHOLD
132 },
133 PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
134 PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
135 PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
136 PersonalityTrait::Sociable => {
137 self.agreeableness > Personality::HIGH_THRESHOLD
138 && self.extraversion > Personality::MID
139 },
140 PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
141 PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
142 PersonalityTrait::Seeker => {
143 self.neuroticism > Personality::HIGH_THRESHOLD
144 && self.openness > Personality::LITTLE_HIGH
145 },
146 PersonalityTrait::Worried => {
147 self.neuroticism > Personality::HIGH_THRESHOLD
148 && self.agreeableness > Personality::LITTLE_HIGH
149 },
150 PersonalityTrait::SadLoner => {
151 self.neuroticism > Personality::HIGH_THRESHOLD
152 && self.extraversion < Personality::LITTLE_LOW
153 },
154 PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
155 }
156 }
157
158 pub fn chat_trait(&self, rng: &mut impl RngExt) -> Option<PersonalityTrait> {
159 PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
160 }
161
162 pub fn will_ambush(&self) -> bool {
163 self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
164 }
165
166 pub fn get_generic_comment(&self, rng: &mut impl RngExt) -> Content {
167 let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
168 match extreme_trait {
169 PersonalityTrait::Open => "npc-speech-villager_open",
170 PersonalityTrait::Adventurous => "npc-speech-villager_adventurous",
171 PersonalityTrait::Closed => "npc-speech-villager_closed",
172 PersonalityTrait::Conscientious => "npc-speech-villager_conscientious",
173 PersonalityTrait::Busybody => "npc-speech-villager_busybody",
174 PersonalityTrait::Unconscientious => "npc-speech-villager_unconscientious",
175 PersonalityTrait::Extroverted => "npc-speech-villager_extroverted",
176 PersonalityTrait::Introverted => "npc-speech-villager_introverted",
177 PersonalityTrait::Agreeable => "npc-speech-villager_agreeable",
178 PersonalityTrait::Sociable => "npc-speech-villager_sociable",
179 PersonalityTrait::Disagreeable => "npc-speech-villager_disagreeable",
180 PersonalityTrait::Neurotic => "npc-speech-villager_neurotic",
181 PersonalityTrait::Seeker => "npc-speech-villager_seeker",
182 PersonalityTrait::SadLoner => "npc-speech-villager_sad_loner",
183 PersonalityTrait::Worried => "npc-speech-villager_worried",
184 PersonalityTrait::Stable => "npc-speech-villager_stable",
185 }
186 } else {
187 "npc-speech-villager"
188 };
189
190 Content::localized(i18n_key)
191 }
192}
193
194impl Default for Personality {
195 fn default() -> Self {
196 Self {
197 openness: Personality::MID,
198 conscientiousness: Personality::MID,
199 extraversion: Personality::MID,
200 agreeableness: Personality::MID,
201 neuroticism: Personality::MID,
202 }
203 }
204}
205
206#[derive(Clone, Debug, Default)]
216pub struct RtSimController {
217 pub activity: Option<NpcActivity>,
218 pub actions: VecDeque<NpcAction>,
219 pub personality: Personality,
220 pub heading_to: Option<String>,
221 pub look_dir: Option<Dir>,
223}
224
225impl RtSimController {
226 pub fn with_destination(pos: Vec3<f32>) -> Self {
227 Self {
228 activity: Some(NpcActivity::Goto(pos, 0.5)),
229 ..Default::default()
230 }
231 }
232}
233
234#[derive(Clone, Copy, Debug)]
235pub enum NpcActivity {
236 Goto(Vec3<f32>, f32),
238 GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
241 Gather(&'static [TerrainResource]),
242 HuntAnimals,
244 Dance(Option<Dir>),
245 Cheer(Option<Dir>),
246 Sit(Option<Dir>, Option<Vec3<i32>>),
247 Talk(ActorId),
248}
249
250#[derive(Clone, Debug)]
253pub enum NpcAction {
254 Say(Option<ActorId>, Content),
258 Attack(ActorId),
260 Dialogue(ActorId, Dialogue),
261 Msg {
263 to: ActorId,
264 msg: NpcMsg,
265 },
266}
267
268#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
269pub struct DialogueId(pub u64);
270
271#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
275pub struct Dialogue<const IS_VALIDATED: bool = false> {
276 pub id: DialogueId,
277 pub kind: DialogueKind,
278}
279impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
280
281impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
282 pub fn message(&self) -> Option<&Content> {
283 match &self.kind {
284 DialogueKind::Start | DialogueKind::End | DialogueKind::Marker { .. } => None,
285 DialogueKind::Statement { msg, .. } | DialogueKind::Question { msg, .. } => Some(msg),
286 DialogueKind::Response { response, .. } => Some(&response.msg),
287 DialogueKind::Ack { .. } => None,
288 }
289 }
290}
291
292impl Dialogue<false> {
293 pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
294}
295
296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
297pub enum DialogueKind {
298 Start,
299 End,
300 Statement {
301 msg: Content,
302 given_item: Option<(Arc<ItemDef>, u32)>,
303 tag: u32,
304 },
305 Ack {
307 tag: u32,
308 },
309 Question {
310 tag: u32,
312 msg: Content,
313 responses: Vec<(u16, Response)>,
315 },
316 Response {
317 tag: u32,
319 response: Response,
320 response_id: u16,
321 },
322 Marker(Marker),
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(ActorId),
345 Dialogue(ActorId, Dialogue<true>),
346 Msg { from: ActorId, msg: NpcMsg },
348}
349
350#[derive(Clone, Debug)]
352pub enum NpcMsg {
353 RequestHire,
355 EndHire,
358}
359
360#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
388pub enum TerrainResource {
389 #[serde(rename = "0")]
390 Grass,
391 #[serde(rename = "1")]
392 Flower,
393 #[serde(rename = "2")]
394 Fruit,
395 #[serde(rename = "3")]
396 Vegetable,
397 #[serde(rename = "4")]
398 Mushroom,
399 #[serde(rename = "5")]
400 Loot, #[serde(rename = "6")]
402 Plant, #[serde(rename = "7")]
404 Stone,
405 #[serde(rename = "8")]
406 Wood, #[serde(rename = "9")]
408 Gem, #[serde(rename = "a")]
410 Ore, }
412
413#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
425pub enum ItemResource {
426 #[serde(rename = "0")]
427 Coin,
428}
429
430impl ItemResource {
431 pub fn to_equivalent_item_def(&self) -> Arc<ItemDef> {
435 match self {
436 Self::Coin => Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
437 }
438 }
439}
440
441#[derive(Clone, Debug, Serialize, Deserialize)]
444pub enum Role {
445 #[serde(rename = "0")]
446 Civilised(Option<Profession>),
447 #[serde(rename = "1")]
448 Wild,
449 #[serde(rename = "2")]
450 Monster,
451 #[serde(rename = "3")]
452 Vehicle,
453}
454
455#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
458pub enum Profession {
459 #[serde(rename = "0")]
460 Farmer,
461 #[serde(rename = "1")]
462 Hunter,
463 #[serde(rename = "2")]
464 Merchant,
465 #[serde(rename = "3")]
466 Guard,
467 #[serde(rename = "4")]
468 Adventurer(u32),
469 #[serde(rename = "5")]
470 Blacksmith,
471 #[serde(rename = "6")]
472 Chef,
473 #[serde(rename = "7")]
474 Alchemist,
475 #[serde(rename = "8")]
476 Pirate(bool),
478 #[serde(rename = "9")]
479 Cultist,
480 #[serde(rename = "10")]
481 Herbalist,
482 #[serde(rename = "11")]
483 Captain,
484}
485
486#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct WorldSettings {
488 pub start_time: f64,
489}
490
491impl Default for WorldSettings {
492 fn default() -> Self {
493 Self {
494 start_time: 9.0 * 3600.0, }
496 }
497}