veloren_common/
rtsim.rs

1// We'd like to not have this file in `common`, but sadly there are
2// things in `common` that require it (currently, `ServerEvent` and
3// `Agent`). When possible, this should be moved to the `rtsim`
4// module in `server`.
5
6use 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/// This type is the map route through which the rtsim (real-time simulation)
222/// aspect of the game communicates with the rest of the game. It is analagous
223/// to `comp::Controller` in that it provides a consistent interface for
224/// simulation NPCs to control their actions. Unlike `comp::Controller`, it is
225/// very abstract and is intended for consumption by both the agent code and the
226/// internal rtsim simulation code (depending on whether the entity is loaded
227/// into the game as a physical entity or not). Agent code should attempt to act
228/// upon its instructions where reasonable although deviations for various
229/// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
230#[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    // TODO: Maybe this should allow for looking at a specific entity target?
237    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    /// (travel_to, speed_factor)
252    Goto(Vec3<f32>, f32),
253    /// (travel_to, speed_factor, height above terrain, direction_override,
254    /// flight_mode)
255    GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
256    Gather(&'static [ChunkResource]),
257    // TODO: Generalise to other entities? What kinds of animals?
258    HuntAnimals,
259    Dance(Option<Dir>),
260    Cheer(Option<Dir>),
261    Sit(Option<Dir>, Option<Vec3<i32>>),
262    Talk(Actor),
263}
264
265/// Represents event-like actions that rtsim NPCs can perform to interact with
266/// the world
267#[derive(Clone, Debug)]
268pub enum NpcAction {
269    /// Speak the given message, with an optional target for that speech.
270    // TODO: Use some sort of structured, language-independent value that frontends can translate
271    // instead
272    Say(Option<Actor>, Content),
273    /// Attack the given target
274    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// `IS_VALIDATED` denotes whether the server has validated this dialogue as
282// fulfilled. For example, a dialogue could promise to give the receiver items
283// from their inventory.
284#[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        // Used to uniquely track each question/response
312        tag: u32,
313        msg: Content,
314        // Response options for the target (response_id, content)
315        responses: Vec<(u16, Response)>,
316    },
317    Response {
318        // Used to uniquely track each question/response
319        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// Represents a message passed back to rtsim from an agent's brain
341#[derive(Clone, Debug)]
342pub enum NpcInput {
343    Report(ReportId),
344    Interaction(Actor, Subject),
345    Dialogue(Actor, Dialogue<true>),
346}
347
348// Note: the `serde(name = "...")` is to minimise the length of field
349// identifiers for the sake of rtsim persistence
350#[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, // Chests, boxes, potions, etc.
364    #[serde(rename = "6")]
365    Plant, // Flax, cotton, wheat, corn, etc.
366    #[serde(rename = "7")]
367    Stone,
368    #[serde(rename = "8")]
369    Wood, // Twigs, logs, bamboo, etc.
370    #[serde(rename = "9")]
371    Gem, // Amethyst, diamond, etc.
372    #[serde(rename = "a")]
373    Ore, // Iron, copper, etc.
374}
375
376// Note: the `serde(name = "...")` is to minimise the length of field
377// identifiers for the sake of rtsim persistence
378#[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// Note: the `serde(name = "...")` is to minimise the length of field
391// identifiers for the sake of rtsim persistence
392#[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, // 9am
429        }
430    }
431}