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}
289
290#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
291pub struct DialogueId(pub u64);
292
293#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
297pub struct Dialogue<const IS_VALIDATED: bool = false> {
298 pub id: DialogueId,
299 pub kind: DialogueKind,
300}
301impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
302
303impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
304 pub fn message(&self) -> Option<&Content> {
305 match &self.kind {
306 DialogueKind::Start | DialogueKind::End | DialogueKind::Marker { .. } => None,
307 DialogueKind::Statement { msg, .. } | DialogueKind::Question { msg, .. } => Some(msg),
308 DialogueKind::Response { response, .. } => Some(&response.msg),
309 DialogueKind::Ack { .. } => None,
310 }
311 }
312}
313
314impl Dialogue<false> {
315 pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
316}
317
318#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
319pub enum DialogueKind {
320 Start,
321 End,
322 Statement {
323 msg: Content,
324 given_item: Option<(Arc<ItemDef>, u32)>,
325 tag: u32,
326 },
327 Ack {
329 tag: u32,
330 },
331 Question {
332 tag: u32,
334 msg: Content,
335 responses: Vec<(u16, Response)>,
337 },
338 Response {
339 tag: u32,
341 response: Response,
342 response_id: u16,
343 },
344 Marker(Marker),
345}
346
347#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
348pub struct Response {
349 pub msg: Content,
350 pub given_item: Option<(Arc<ItemDef>, u32)>,
351}
352
353impl From<Content> for Response {
354 fn from(msg: Content) -> Self {
355 Self {
356 msg,
357 given_item: None,
358 }
359 }
360}
361
362#[derive(Clone, Debug)]
364pub enum NpcInput {
365 Report(ReportId),
366 Interaction(Actor),
367 Dialogue(Actor, Dialogue<true>),
368}
369
370#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
398pub enum TerrainResource {
399 #[serde(rename = "0")]
400 Grass,
401 #[serde(rename = "1")]
402 Flower,
403 #[serde(rename = "2")]
404 Fruit,
405 #[serde(rename = "3")]
406 Vegetable,
407 #[serde(rename = "4")]
408 Mushroom,
409 #[serde(rename = "5")]
410 Loot, #[serde(rename = "6")]
412 Plant, #[serde(rename = "7")]
414 Stone,
415 #[serde(rename = "8")]
416 Wood, #[serde(rename = "9")]
418 Gem, #[serde(rename = "a")]
420 Ore, }
422
423#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
435pub enum ItemResource {
436 #[serde(rename = "0")]
437 Coin,
438}
439
440impl ItemResource {
441 pub fn to_equivalent_item_def(&self) -> Arc<ItemDef> {
445 match self {
446 Self::Coin => Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
447 }
448 }
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize)]
454pub enum Role {
455 #[serde(rename = "0")]
456 Civilised(Option<Profession>),
457 #[serde(rename = "1")]
458 Wild,
459 #[serde(rename = "2")]
460 Monster,
461 #[serde(rename = "3")]
462 Vehicle,
463}
464
465#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
468pub enum Profession {
469 #[serde(rename = "0")]
470 Farmer,
471 #[serde(rename = "1")]
472 Hunter,
473 #[serde(rename = "2")]
474 Merchant,
475 #[serde(rename = "3")]
476 Guard,
477 #[serde(rename = "4")]
478 Adventurer(u32),
479 #[serde(rename = "5")]
480 Blacksmith,
481 #[serde(rename = "6")]
482 Chef,
483 #[serde(rename = "7")]
484 Alchemist,
485 #[serde(rename = "8")]
486 Pirate(bool),
488 #[serde(rename = "9")]
489 Cultist,
490 #[serde(rename = "10")]
491 Herbalist,
492 #[serde(rename = "11")]
493 Captain,
494}
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
497pub struct WorldSettings {
498 pub start_time: f64,
499}
500
501impl Default for WorldSettings {
502 fn default() -> Self {
503 Self {
504 start_time: 9.0 * 3600.0, }
506 }
507}