1use crate::{
17 assets::AssetExt,
18 character::CharacterId,
19 comp::{agent::FlightMode, inventory::item::ItemDef},
20 map::Marker,
21 util::Dir,
22};
23use common_i18n::Content;
24use rand::{Rng, seq::IteratorRandom};
25use serde::{Deserialize, Serialize};
26use specs::Component;
27use std::{collections::VecDeque, sync::Arc};
28use strum::{EnumIter, IntoEnumIterator};
29use vek::*;
30
31slotmap::new_key_type! { pub struct NpcId; }
32
33slotmap::new_key_type! { pub struct SiteId; }
34
35slotmap::new_key_type! { pub struct FactionId; }
36
37slotmap::new_key_type! { pub struct ReportId; }
38
39#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
40pub struct QuestId(pub u64);
41
42pub type RtSimEntity = NpcId; impl Component for RtSimEntity {
45 type Storage = specs::VecStorage<Self>;
46}
47
48#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
49pub enum Actor {
50 Npc(NpcId),
51 Character(CharacterId),
52}
53
54impl Actor {
55 pub fn npc(&self) -> Option<NpcId> {
56 match self {
57 Actor::Npc(id) => Some(*id),
58 Actor::Character(_) => None,
59 }
60 }
61}
62
63impl From<NpcId> for Actor {
64 fn from(value: NpcId) -> Self { Actor::Npc(value) }
65}
66
67impl From<CharacterId> for Actor {
68 fn from(value: CharacterId) -> Self { Actor::Character(value) }
69}
70
71#[derive(EnumIter, Clone, Copy)]
72pub enum PersonalityTrait {
73 Open,
74 Adventurous,
75 Closed,
76 Conscientious,
77 Busybody,
78 Unconscientious,
79 Extroverted,
80 Introverted,
81 Agreeable,
82 Sociable,
83 Disagreeable,
84 Neurotic,
85 Seeker,
86 Worried,
87 SadLoner,
88 Stable,
89}
90
91#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
92pub struct Personality {
93 openness: u8,
94 conscientiousness: u8,
95 extraversion: u8,
96 agreeableness: u8,
97 neuroticism: u8,
98}
99
100fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 {
101 let l = max - min;
102 min + rng.random_range(0..=l / 3)
103 + rng.random_range(0..=l / 3 + l % 3 % 2)
104 + rng.random_range(0..=l / 3 + l % 3 / 2)
105}
106
107impl Personality {
108 pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
109 pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
110 pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
111 pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
112 const MAX: u8 = 255;
113 pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
114 const MIN: u8 = 0;
115
116 fn distributed_value(rng: &mut impl Rng) -> u8 { distributed(Self::MIN, Self::MAX, rng) }
117
118 pub fn random(rng: &mut impl Rng) -> Self {
119 Self {
120 openness: Self::distributed_value(rng),
121 conscientiousness: Self::distributed_value(rng),
122 extraversion: Self::distributed_value(rng),
123 agreeableness: Self::distributed_value(rng),
124 neuroticism: Self::distributed_value(rng),
125 }
126 }
127
128 pub fn random_evil(rng: &mut impl Rng) -> Self {
129 Self {
130 openness: Self::distributed_value(rng),
131 extraversion: Self::distributed_value(rng),
132 neuroticism: Self::distributed_value(rng),
133 agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
134 conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
135 }
136 }
137
138 pub fn random_good(rng: &mut impl Rng) -> Self {
139 Self {
140 openness: Self::distributed_value(rng),
141 extraversion: Self::distributed_value(rng),
142 neuroticism: Self::distributed_value(rng),
143 agreeableness: Self::distributed_value(rng),
144 conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
145 }
146 }
147
148 pub fn is(&self, trait_: PersonalityTrait) -> bool {
149 match trait_ {
150 PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
151 PersonalityTrait::Adventurous => {
152 self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID
153 },
154 PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
155 PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
156 PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
157 PersonalityTrait::Unconscientious => {
158 self.conscientiousness < Personality::LOW_THRESHOLD
159 },
160 PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
161 PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
162 PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
163 PersonalityTrait::Sociable => {
164 self.agreeableness > Personality::HIGH_THRESHOLD
165 && self.extraversion > Personality::MID
166 },
167 PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
168 PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
169 PersonalityTrait::Seeker => {
170 self.neuroticism > Personality::HIGH_THRESHOLD
171 && self.openness > Personality::LITTLE_HIGH
172 },
173 PersonalityTrait::Worried => {
174 self.neuroticism > Personality::HIGH_THRESHOLD
175 && self.agreeableness > Personality::LITTLE_HIGH
176 },
177 PersonalityTrait::SadLoner => {
178 self.neuroticism > Personality::HIGH_THRESHOLD
179 && self.extraversion < Personality::LITTLE_LOW
180 },
181 PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
182 }
183 }
184
185 pub fn chat_trait(&self, rng: &mut impl Rng) -> Option<PersonalityTrait> {
186 PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
187 }
188
189 pub fn will_ambush(&self) -> bool {
190 self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
191 }
192
193 pub fn get_generic_comment(&self, rng: &mut impl Rng) -> Content {
194 let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
195 match extreme_trait {
196 PersonalityTrait::Open => "npc-speech-villager_open",
197 PersonalityTrait::Adventurous => "npc-speech-villager_adventurous",
198 PersonalityTrait::Closed => "npc-speech-villager_closed",
199 PersonalityTrait::Conscientious => "npc-speech-villager_conscientious",
200 PersonalityTrait::Busybody => "npc-speech-villager_busybody",
201 PersonalityTrait::Unconscientious => "npc-speech-villager_unconscientious",
202 PersonalityTrait::Extroverted => "npc-speech-villager_extroverted",
203 PersonalityTrait::Introverted => "npc-speech-villager_introverted",
204 PersonalityTrait::Agreeable => "npc-speech-villager_agreeable",
205 PersonalityTrait::Sociable => "npc-speech-villager_sociable",
206 PersonalityTrait::Disagreeable => "npc-speech-villager_disagreeable",
207 PersonalityTrait::Neurotic => "npc-speech-villager_neurotic",
208 PersonalityTrait::Seeker => "npc-speech-villager_seeker",
209 PersonalityTrait::SadLoner => "npc-speech-villager_sad_loner",
210 PersonalityTrait::Worried => "npc-speech-villager_worried",
211 PersonalityTrait::Stable => "npc-speech-villager_stable",
212 }
213 } else {
214 "npc-speech-villager"
215 };
216
217 Content::localized(i18n_key)
218 }
219}
220
221impl Default for Personality {
222 fn default() -> Self {
223 Self {
224 openness: Personality::MID,
225 conscientiousness: Personality::MID,
226 extraversion: Personality::MID,
227 agreeableness: Personality::MID,
228 neuroticism: Personality::MID,
229 }
230 }
231}
232
233#[derive(Clone, Debug, Default)]
243pub struct RtSimController {
244 pub activity: Option<NpcActivity>,
245 pub actions: VecDeque<NpcAction>,
246 pub personality: Personality,
247 pub heading_to: Option<String>,
248 pub look_dir: Option<Dir>,
250}
251
252impl RtSimController {
253 pub fn with_destination(pos: Vec3<f32>) -> Self {
254 Self {
255 activity: Some(NpcActivity::Goto(pos, 0.5)),
256 ..Default::default()
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug)]
262pub enum NpcActivity {
263 Goto(Vec3<f32>, f32),
265 GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
268 Gather(&'static [TerrainResource]),
269 HuntAnimals,
271 Dance(Option<Dir>),
272 Cheer(Option<Dir>),
273 Sit(Option<Dir>, Option<Vec3<i32>>),
274 Talk(Actor),
275}
276
277#[derive(Clone, Debug)]
280pub enum NpcAction {
281 Say(Option<Actor>, Content),
285 Attack(Actor),
287 Dialogue(Actor, Dialogue),
288 Msg {
290 to: Actor,
291 msg: NpcMsg,
292 },
293}
294
295#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
296pub struct DialogueId(pub u64);
297
298#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302pub struct Dialogue<const IS_VALIDATED: bool = false> {
303 pub id: DialogueId,
304 pub kind: DialogueKind,
305}
306impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
307
308impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
309 pub fn message(&self) -> Option<&Content> {
310 match &self.kind {
311 DialogueKind::Start | DialogueKind::End | DialogueKind::Marker { .. } => None,
312 DialogueKind::Statement { msg, .. } | DialogueKind::Question { msg, .. } => Some(msg),
313 DialogueKind::Response { response, .. } => Some(&response.msg),
314 DialogueKind::Ack { .. } => None,
315 }
316 }
317}
318
319impl Dialogue<false> {
320 pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
321}
322
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
324pub enum DialogueKind {
325 Start,
326 End,
327 Statement {
328 msg: Content,
329 given_item: Option<(Arc<ItemDef>, u32)>,
330 tag: u32,
331 },
332 Ack {
334 tag: u32,
335 },
336 Question {
337 tag: u32,
339 msg: Content,
340 responses: Vec<(u16, Response)>,
342 },
343 Response {
344 tag: u32,
346 response: Response,
347 response_id: u16,
348 },
349 Marker(Marker),
350}
351
352#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
353pub struct Response {
354 pub msg: Content,
355 pub given_item: Option<(Arc<ItemDef>, u32)>,
356}
357
358impl From<Content> for Response {
359 fn from(msg: Content) -> Self {
360 Self {
361 msg,
362 given_item: None,
363 }
364 }
365}
366
367#[derive(Clone, Debug)]
369pub enum NpcInput {
370 Report(ReportId),
371 Interaction(Actor),
372 Dialogue(Actor, Dialogue<true>),
373 Msg { from: Actor, msg: NpcMsg },
375}
376
377#[derive(Clone, Debug)]
379pub enum NpcMsg {
380 RequestHire,
382 EndHire,
385}
386
387#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
415pub enum TerrainResource {
416 #[serde(rename = "0")]
417 Grass,
418 #[serde(rename = "1")]
419 Flower,
420 #[serde(rename = "2")]
421 Fruit,
422 #[serde(rename = "3")]
423 Vegetable,
424 #[serde(rename = "4")]
425 Mushroom,
426 #[serde(rename = "5")]
427 Loot, #[serde(rename = "6")]
429 Plant, #[serde(rename = "7")]
431 Stone,
432 #[serde(rename = "8")]
433 Wood, #[serde(rename = "9")]
435 Gem, #[serde(rename = "a")]
437 Ore, }
439
440#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
452pub enum ItemResource {
453 #[serde(rename = "0")]
454 Coin,
455}
456
457impl ItemResource {
458 pub fn to_equivalent_item_def(&self) -> Arc<ItemDef> {
462 match self {
463 Self::Coin => Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
464 }
465 }
466}
467
468#[derive(Clone, Debug, Serialize, Deserialize)]
471pub enum Role {
472 #[serde(rename = "0")]
473 Civilised(Option<Profession>),
474 #[serde(rename = "1")]
475 Wild,
476 #[serde(rename = "2")]
477 Monster,
478 #[serde(rename = "3")]
479 Vehicle,
480}
481
482#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
485pub enum Profession {
486 #[serde(rename = "0")]
487 Farmer,
488 #[serde(rename = "1")]
489 Hunter,
490 #[serde(rename = "2")]
491 Merchant,
492 #[serde(rename = "3")]
493 Guard,
494 #[serde(rename = "4")]
495 Adventurer(u32),
496 #[serde(rename = "5")]
497 Blacksmith,
498 #[serde(rename = "6")]
499 Chef,
500 #[serde(rename = "7")]
501 Alchemist,
502 #[serde(rename = "8")]
503 Pirate(bool),
505 #[serde(rename = "9")]
506 Cultist,
507 #[serde(rename = "10")]
508 Herbalist,
509 #[serde(rename = "11")]
510 Captain,
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize)]
514pub struct WorldSettings {
515 pub start_time: f64,
516}
517
518impl Default for WorldSettings {
519 fn default() -> Self {
520 Self {
521 start_time: 9.0 * 3600.0, }
523 }
524}