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