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