veloren_common/
rtsim.rs

1//! Type definitions used for interfacing between rtsim and the rest of the
2//! game.
3//!
4//! See the `veloren_rtsim` crate for an in-depth explanation as to what rtsim
5//! is and how it works.
6//!
7//! The types in this module generally come in a few flavours:
8//!
9//! - IDs like [`NpcId`] and [`SiteId`], used to address objects that are shared
10//!   between both domains
11//! - Messages like [`Dialogue`] and [`NpcAction`] which facilitate
12//!   communication between both domains
13//! - 'Resource duals' like [`TerrainResource`] that allow physical items or
14//!   resources to be translated between domains (often lossily)
15
16use 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; // TODO: Remove this, alias is needed for historical reasons
43
44impl 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/// This type is the map route through which the rtsim (real-time simulation)
234/// aspect of the game communicates with the rest of the game. It is analagous
235/// to `comp::Controller` in that it provides a consistent interface for
236/// simulation NPCs to control their actions. Unlike `comp::Controller`, it is
237/// very abstract and is intended for consumption by both the agent code and the
238/// internal rtsim simulation code (depending on whether the entity is loaded
239/// into the game as a physical entity or not). Agent code should attempt to act
240/// upon its instructions where reasonable although deviations for various
241/// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
242#[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    // TODO: Maybe this should allow for looking at a specific entity target?
249    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    /// (travel_to, speed_factor)
264    Goto(Vec3<f32>, f32),
265    /// (travel_to, speed_factor, height above terrain, direction_override,
266    /// flight_mode)
267    GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
268    Gather(&'static [TerrainResource]),
269    // TODO: Generalise to other entities? What kinds of animals?
270    HuntAnimals,
271    Dance(Option<Dir>),
272    Cheer(Option<Dir>),
273    Sit(Option<Dir>, Option<Vec3<i32>>),
274    Talk(Actor),
275}
276
277/// Represents event-like actions that rtsim NPCs can perform to interact with
278/// the world
279#[derive(Clone, Debug)]
280pub enum NpcAction {
281    /// Speak the given message, with an optional target for that speech.
282    // TODO: Use some sort of structured, language-independent value that frontends can translate
283    // instead
284    Say(Option<Actor>, Content),
285    /// Attack the given target
286    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// `IS_VALIDATED` denotes whether the server has validated this dialogue as
294// fulfilled. For example, a dialogue could promise to give the receiver items
295// from their inventory.
296#[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    // Acknowledge a statement, allowing the conversation to proceed
328    Ack {
329        tag: u32,
330    },
331    Question {
332        // Used to uniquely track each question/response
333        tag: u32,
334        msg: Content,
335        // Response options for the target (response_id, content)
336        responses: Vec<(u16, Response)>,
337    },
338    Response {
339        // Used to uniquely track each question/response
340        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// Represents a message passed back to rtsim from an agent's brain
363#[derive(Clone, Debug)]
364pub enum NpcInput {
365    Report(ReportId),
366    Interaction(Actor),
367    Dialogue(Actor, Dialogue<true>),
368}
369
370/// Abstractly represents categories of resources that might naturally appear in
371/// the world as a product of world generation.
372///
373/// Representing resources abstractly in this way allows us to decouple rtsim
374/// from the rest of the game so that we don't have to include things like
375/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`] in rtsim's
376/// persistence data model (which would require non-trivial migration work if
377/// those enums and their attributes change over time).
378///
379/// Terrain resources are usually tracked per-chunk, but this is not always
380/// true. For example, a site might contain farm fields, and those fields might
381/// track their resources independently of the chunks they appear in at some
382/// future stage of development.
383///
384/// You can determine the rtsim resource represented by a block with
385/// [`common::terrain::Block::get_rtsim_resource`].
386///
387/// Going in the other direction is necessarily a stochastic endeavour. For
388/// example, both `SpriteKind::Diamond` and `SpriteKind::Amethyst` currently map
389/// to `TerrainResource::Gem`, which is a lossy conversion: to go back the other
390/// way, we have to pick one or the other with some probability. It might be
391/// desirable to weight this probability according to the commonly accepted
392/// scarcity/value of the item to avoid balancing issues.
393///
394/// If you want to track inventory items with rtsim, see [`ItemResource`].
395// Note: the `serde(name = "...")` is to minimise the length of field
396// identifiers for the sake of rtsim persistence
397#[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, // Chests, boxes, potions, etc.
411    #[serde(rename = "6")]
412    Plant, // Flax, cotton, wheat, corn, etc.
413    #[serde(rename = "7")]
414    Stone,
415    #[serde(rename = "8")]
416    Wood, // Twigs, logs, bamboo, etc.
417    #[serde(rename = "9")]
418    Gem, // Amethyst, diamond, etc.
419    #[serde(rename = "a")]
420    Ore, // Iron, copper, etc.
421}
422
423/// Like [`TerrainResource`], but for tracking inventory items in rtsim for the
424/// sake of questing, trade, etc.
425///
426/// This type is a conceptual dual of [`TerrainResource`], so most of the same
427/// ideas apply. It is to [`common::comp::Item`] what [`TerrainResource`] is to
428/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`].
429///
430/// | In-game types             | Rtsim representation |
431/// |---------------------------|----------------------|
432/// | `Item`                    | `ItemResource`       |
433/// | `SpriteKind`, `BlockKind` | `TerrainResource`    |
434#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
435pub enum ItemResource {
436    #[serde(rename = "0")]
437    Coin,
438}
439
440impl ItemResource {
441    /// Attempt to translate this resource into an equivalent [`ItemDef`].
442    // TODO: Return (Arc<ItemDef>, f32) to allow for an exchange rate
443    // TODO: Have this function take an `impl Rng` so that it can be stochastic
444    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// Note: the `serde(name = "...")` is to minimise the length of field
452// identifiers for the sake of rtsim persistence
453#[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// Note: the `serde(name = "...")` is to minimise the length of field
466// identifiers for the sake of rtsim persistence
467#[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    /// True if leader
487    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, // 9am
505        }
506    }
507}