Skip to main content

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 [`ActorId`] and [`SiteId`], used to address objects that are
10//!   shared 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    comp::{agent::FlightMode, inventory::item::ItemDef},
19    map::Marker,
20    util::Dir,
21};
22use common_i18n::Content;
23use rand::{RngExt, seq::IteratorRandom};
24use serde::{Deserialize, Serialize};
25use specs::Component;
26use std::{collections::VecDeque, sync::Arc};
27use strum::{EnumIter, IntoEnumIterator};
28use vek::*;
29
30slotmap::new_key_type! {
31    pub struct ActorId;
32    pub struct SiteId;
33    pub struct FactionId;
34    pub struct ReportId;
35}
36
37#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
38pub struct QuestId(pub u64);
39
40impl Component for ActorId {
41    type Storage = specs::VecStorage<Self>;
42}
43
44#[derive(EnumIter, Clone, Copy)]
45pub enum PersonalityTrait {
46    Open,
47    Adventurous,
48    Closed,
49    Conscientious,
50    Busybody,
51    Unconscientious,
52    Extroverted,
53    Introverted,
54    Agreeable,
55    Sociable,
56    Disagreeable,
57    Neurotic,
58    Seeker,
59    Worried,
60    SadLoner,
61    Stable,
62}
63
64#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
65pub struct Personality {
66    openness: u8,
67    conscientiousness: u8,
68    extraversion: u8,
69    agreeableness: u8,
70    neuroticism: u8,
71}
72
73fn distributed(min: u8, max: u8, rng: &mut impl RngExt) -> u8 {
74    let l = max - min;
75    min + rng.random_range(0..=l / 3)
76        + rng.random_range(0..=l / 3 + l % 3 % 2)
77        + rng.random_range(0..=l / 3 + l % 3 / 2)
78}
79
80impl Personality {
81    pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
82    pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
83    pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
84    pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
85    const MAX: u8 = 255;
86    pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
87    const MIN: u8 = 0;
88
89    fn distributed_value(rng: &mut impl RngExt) -> u8 { distributed(Self::MIN, Self::MAX, rng) }
90
91    pub fn random(rng: &mut impl RngExt) -> Self {
92        Self {
93            openness: Self::distributed_value(rng),
94            conscientiousness: Self::distributed_value(rng),
95            extraversion: Self::distributed_value(rng),
96            agreeableness: Self::distributed_value(rng),
97            neuroticism: Self::distributed_value(rng),
98        }
99    }
100
101    pub fn random_evil(rng: &mut impl RngExt) -> Self {
102        Self {
103            openness: Self::distributed_value(rng),
104            extraversion: Self::distributed_value(rng),
105            neuroticism: Self::distributed_value(rng),
106            agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
107            conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
108        }
109    }
110
111    pub fn random_good(rng: &mut impl RngExt) -> Self {
112        Self {
113            openness: Self::distributed_value(rng),
114            extraversion: Self::distributed_value(rng),
115            neuroticism: Self::distributed_value(rng),
116            agreeableness: Self::distributed_value(rng),
117            conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
118        }
119    }
120
121    pub fn is(&self, trait_: PersonalityTrait) -> bool {
122        match trait_ {
123            PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
124            PersonalityTrait::Adventurous => {
125                self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID
126            },
127            PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
128            PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
129            PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
130            PersonalityTrait::Unconscientious => {
131                self.conscientiousness < Personality::LOW_THRESHOLD
132            },
133            PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
134            PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
135            PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
136            PersonalityTrait::Sociable => {
137                self.agreeableness > Personality::HIGH_THRESHOLD
138                    && self.extraversion > Personality::MID
139            },
140            PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
141            PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
142            PersonalityTrait::Seeker => {
143                self.neuroticism > Personality::HIGH_THRESHOLD
144                    && self.openness > Personality::LITTLE_HIGH
145            },
146            PersonalityTrait::Worried => {
147                self.neuroticism > Personality::HIGH_THRESHOLD
148                    && self.agreeableness > Personality::LITTLE_HIGH
149            },
150            PersonalityTrait::SadLoner => {
151                self.neuroticism > Personality::HIGH_THRESHOLD
152                    && self.extraversion < Personality::LITTLE_LOW
153            },
154            PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
155        }
156    }
157
158    pub fn chat_trait(&self, rng: &mut impl RngExt) -> Option<PersonalityTrait> {
159        PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
160    }
161
162    pub fn will_ambush(&self) -> bool {
163        self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
164    }
165
166    pub fn get_generic_comment(&self, rng: &mut impl RngExt) -> Content {
167        let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
168            match extreme_trait {
169                PersonalityTrait::Open => "npc-speech-villager_open",
170                PersonalityTrait::Adventurous => "npc-speech-villager_adventurous",
171                PersonalityTrait::Closed => "npc-speech-villager_closed",
172                PersonalityTrait::Conscientious => "npc-speech-villager_conscientious",
173                PersonalityTrait::Busybody => "npc-speech-villager_busybody",
174                PersonalityTrait::Unconscientious => "npc-speech-villager_unconscientious",
175                PersonalityTrait::Extroverted => "npc-speech-villager_extroverted",
176                PersonalityTrait::Introverted => "npc-speech-villager_introverted",
177                PersonalityTrait::Agreeable => "npc-speech-villager_agreeable",
178                PersonalityTrait::Sociable => "npc-speech-villager_sociable",
179                PersonalityTrait::Disagreeable => "npc-speech-villager_disagreeable",
180                PersonalityTrait::Neurotic => "npc-speech-villager_neurotic",
181                PersonalityTrait::Seeker => "npc-speech-villager_seeker",
182                PersonalityTrait::SadLoner => "npc-speech-villager_sad_loner",
183                PersonalityTrait::Worried => "npc-speech-villager_worried",
184                PersonalityTrait::Stable => "npc-speech-villager_stable",
185            }
186        } else {
187            "npc-speech-villager"
188        };
189
190        Content::localized(i18n_key)
191    }
192}
193
194impl Default for Personality {
195    fn default() -> Self {
196        Self {
197            openness: Personality::MID,
198            conscientiousness: Personality::MID,
199            extraversion: Personality::MID,
200            agreeableness: Personality::MID,
201            neuroticism: Personality::MID,
202        }
203    }
204}
205
206/// This type is the map route through which the rtsim (real-time simulation)
207/// aspect of the game communicates with the rest of the game. It is analagous
208/// to `comp::Controller` in that it provides a consistent interface for
209/// simulation NPCs to control their actions. Unlike `comp::Controller`, it is
210/// very abstract and is intended for consumption by both the agent code and the
211/// internal rtsim simulation code (depending on whether the entity is loaded
212/// into the game as a physical entity or not). Agent code should attempt to act
213/// upon its instructions where reasonable although deviations for various
214/// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
215#[derive(Clone, Debug, Default)]
216pub struct RtSimController {
217    pub activity: Option<NpcActivity>,
218    pub actions: VecDeque<NpcAction>,
219    pub personality: Personality,
220    pub heading_to: Option<String>,
221    // TODO: Maybe this should allow for looking at a specific entity target?
222    pub look_dir: Option<Dir>,
223}
224
225impl RtSimController {
226    pub fn with_destination(pos: Vec3<f32>) -> Self {
227        Self {
228            activity: Some(NpcActivity::Goto(pos, 0.5)),
229            ..Default::default()
230        }
231    }
232}
233
234#[derive(Clone, Copy, Debug)]
235pub enum NpcActivity {
236    /// (travel_to, speed_factor)
237    Goto(Vec3<f32>, f32),
238    /// (travel_to, speed_factor, height above terrain, direction_override,
239    /// flight_mode)
240    GotoFlying(Vec3<f32>, f32, Option<f32>, Option<Dir>, FlightMode),
241    Gather(&'static [TerrainResource]),
242    // TODO: Generalise to other entities? What kinds of animals?
243    HuntAnimals,
244    Dance(Option<Dir>),
245    Cheer(Option<Dir>),
246    Sit(Option<Dir>, Option<Vec3<i32>>),
247    Talk(ActorId),
248}
249
250/// Represents event-like actions that rtsim NPCs can perform to interact with
251/// the world
252#[derive(Clone, Debug)]
253pub enum NpcAction {
254    /// Speak the given message, with an optional target for that speech.
255    // TODO: Use some sort of structured, language-independent value that frontends can translate
256    // instead
257    Say(Option<ActorId>, Content),
258    /// Attack the given target
259    Attack(ActorId),
260    Dialogue(ActorId, Dialogue),
261    // TODO: Make this more principled, currently only used by pirates
262    Msg {
263        to: ActorId,
264        msg: NpcMsg,
265    },
266}
267
268#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
269pub struct DialogueId(pub u64);
270
271// `IS_VALIDATED` denotes whether the server has validated this dialogue as
272// fulfilled. For example, a dialogue could promise to give the receiver items
273// from their inventory.
274#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
275pub struct Dialogue<const IS_VALIDATED: bool = false> {
276    pub id: DialogueId,
277    pub kind: DialogueKind,
278}
279impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
280
281impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
282    pub fn message(&self) -> Option<&Content> {
283        match &self.kind {
284            DialogueKind::Start | DialogueKind::End | DialogueKind::Marker { .. } => None,
285            DialogueKind::Statement { msg, .. } | DialogueKind::Question { msg, .. } => Some(msg),
286            DialogueKind::Response { response, .. } => Some(&response.msg),
287            DialogueKind::Ack { .. } => None,
288        }
289    }
290}
291
292impl Dialogue<false> {
293    pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
294}
295
296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
297pub enum DialogueKind {
298    Start,
299    End,
300    Statement {
301        msg: Content,
302        given_item: Option<(Arc<ItemDef>, u32)>,
303        tag: u32,
304    },
305    // Acknowledge a statement, allowing the conversation to proceed
306    Ack {
307        tag: u32,
308    },
309    Question {
310        // Used to uniquely track each question/response
311        tag: u32,
312        msg: Content,
313        // Response options for the target (response_id, content)
314        responses: Vec<(u16, Response)>,
315    },
316    Response {
317        // Used to uniquely track each question/response
318        tag: u32,
319        response: Response,
320        response_id: u16,
321    },
322    Marker(Marker),
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(ActorId),
345    Dialogue(ActorId, Dialogue<true>),
346    // TODO: Make this more principled, currently only used by pirates
347    Msg { from: ActorId, msg: NpcMsg },
348}
349
350/// Represents a message that may be communicated between NPCs on a 1:1 basis
351#[derive(Clone, Debug)]
352pub enum NpcMsg {
353    /// Send by an NPC that wants to hire the receiver.
354    RequestHire,
355    /// Sent by an NPC to a hired hand when they wish to end the hiring
356    /// relationship
357    EndHire,
358}
359
360/// Abstractly represents categories of resources that might naturally appear in
361/// the world as a product of world generation.
362///
363/// Representing resources abstractly in this way allows us to decouple rtsim
364/// from the rest of the game so that we don't have to include things like
365/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`] in rtsim's
366/// persistence data model (which would require non-trivial migration work if
367/// those enums and their attributes change over time).
368///
369/// Terrain resources are usually tracked per-chunk, but this is not always
370/// true. For example, a site might contain farm fields, and those fields might
371/// track their resources independently of the chunks they appear in at some
372/// future stage of development.
373///
374/// You can determine the rtsim resource represented by a block with
375/// [`common::terrain::Block::get_rtsim_resource`].
376///
377/// Going in the other direction is necessarily a stochastic endeavour. For
378/// example, both `SpriteKind::Diamond` and `SpriteKind::Amethyst` currently map
379/// to `TerrainResource::Gem`, which is a lossy conversion: to go back the other
380/// way, we have to pick one or the other with some probability. It might be
381/// desirable to weight this probability according to the commonly accepted
382/// scarcity/value of the item to avoid balancing issues.
383///
384/// If you want to track inventory items with rtsim, see [`ItemResource`].
385// Note: the `serde(name = "...")` is to minimise the length of field
386// identifiers for the sake of rtsim persistence
387#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
388pub enum TerrainResource {
389    #[serde(rename = "0")]
390    Grass,
391    #[serde(rename = "1")]
392    Flower,
393    #[serde(rename = "2")]
394    Fruit,
395    #[serde(rename = "3")]
396    Vegetable,
397    #[serde(rename = "4")]
398    Mushroom,
399    #[serde(rename = "5")]
400    Loot, // Chests, boxes, potions, etc.
401    #[serde(rename = "6")]
402    Plant, // Flax, cotton, wheat, corn, etc.
403    #[serde(rename = "7")]
404    Stone,
405    #[serde(rename = "8")]
406    Wood, // Twigs, logs, bamboo, etc.
407    #[serde(rename = "9")]
408    Gem, // Amethyst, diamond, etc.
409    #[serde(rename = "a")]
410    Ore, // Iron, copper, etc.
411}
412
413/// Like [`TerrainResource`], but for tracking inventory items in rtsim for the
414/// sake of questing, trade, etc.
415///
416/// This type is a conceptual dual of [`TerrainResource`], so most of the same
417/// ideas apply. It is to [`common::comp::Item`] what [`TerrainResource`] is to
418/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`].
419///
420/// | In-game types             | Rtsim representation |
421/// |---------------------------|----------------------|
422/// | `Item`                    | `ItemResource`       |
423/// | `SpriteKind`, `BlockKind` | `TerrainResource`    |
424#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
425pub enum ItemResource {
426    #[serde(rename = "0")]
427    Coin,
428}
429
430impl ItemResource {
431    /// Attempt to translate this resource into an equivalent [`ItemDef`].
432    // TODO: Return (Arc<ItemDef>, f32) to allow for an exchange rate
433    // TODO: Have this function take an `impl RngExt` so that it can be stochastic
434    pub fn to_equivalent_item_def(&self) -> Arc<ItemDef> {
435        match self {
436            Self::Coin => Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
437        }
438    }
439}
440
441// Note: the `serde(name = "...")` is to minimise the length of field
442// identifiers for the sake of rtsim persistence
443#[derive(Clone, Debug, Serialize, Deserialize)]
444pub enum Role {
445    #[serde(rename = "0")]
446    Civilised(Option<Profession>),
447    #[serde(rename = "1")]
448    Wild,
449    #[serde(rename = "2")]
450    Monster,
451    #[serde(rename = "3")]
452    Vehicle,
453}
454
455// Note: the `serde(name = "...")` is to minimise the length of field
456// identifiers for the sake of rtsim persistence
457#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
458pub enum Profession {
459    #[serde(rename = "0")]
460    Farmer,
461    #[serde(rename = "1")]
462    Hunter,
463    #[serde(rename = "2")]
464    Merchant,
465    #[serde(rename = "3")]
466    Guard,
467    #[serde(rename = "4")]
468    Adventurer(u32),
469    #[serde(rename = "5")]
470    Blacksmith,
471    #[serde(rename = "6")]
472    Chef,
473    #[serde(rename = "7")]
474    Alchemist,
475    #[serde(rename = "8")]
476    /// True if leader
477    Pirate(bool),
478    #[serde(rename = "9")]
479    Cultist,
480    #[serde(rename = "10")]
481    Herbalist,
482    #[serde(rename = "11")]
483    Captain,
484}
485
486#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct WorldSettings {
488    pub start_time: f64,
489}
490
491impl Default for WorldSettings {
492    fn default() -> Self {
493        Self {
494            start_time: 9.0 * 3600.0, // 9am
495        }
496    }
497}