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    // TODO: Make this more principled, currently only used by pirates
289    Msg {
290        to: Actor,
291        msg: NpcMsg,
292    },
293}
294
295#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
296pub struct DialogueId(pub u64);
297
298// `IS_VALIDATED` denotes whether the server has validated this dialogue as
299// fulfilled. For example, a dialogue could promise to give the receiver items
300// from their inventory.
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302pub struct Dialogue<const IS_VALIDATED: bool = false> {
303    pub id: DialogueId,
304    pub kind: DialogueKind,
305}
306impl<const IS_VALIDATED: bool> core::cmp::Eq for Dialogue<IS_VALIDATED> {}
307
308impl<const IS_VALIDATED: bool> Dialogue<IS_VALIDATED> {
309    pub fn message(&self) -> Option<&Content> {
310        match &self.kind {
311            DialogueKind::Start | DialogueKind::End | DialogueKind::Marker { .. } => None,
312            DialogueKind::Statement { msg, .. } | DialogueKind::Question { msg, .. } => Some(msg),
313            DialogueKind::Response { response, .. } => Some(&response.msg),
314            DialogueKind::Ack { .. } => None,
315        }
316    }
317}
318
319impl Dialogue<false> {
320    pub fn into_validated_unchecked(self) -> Dialogue<true> { Dialogue { ..self } }
321}
322
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
324pub enum DialogueKind {
325    Start,
326    End,
327    Statement {
328        msg: Content,
329        given_item: Option<(Arc<ItemDef>, u32)>,
330        tag: u32,
331    },
332    // Acknowledge a statement, allowing the conversation to proceed
333    Ack {
334        tag: u32,
335    },
336    Question {
337        // Used to uniquely track each question/response
338        tag: u32,
339        msg: Content,
340        // Response options for the target (response_id, content)
341        responses: Vec<(u16, Response)>,
342    },
343    Response {
344        // Used to uniquely track each question/response
345        tag: u32,
346        response: Response,
347        response_id: u16,
348    },
349    Marker(Marker),
350}
351
352#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
353pub struct Response {
354    pub msg: Content,
355    pub given_item: Option<(Arc<ItemDef>, u32)>,
356}
357
358impl From<Content> for Response {
359    fn from(msg: Content) -> Self {
360        Self {
361            msg,
362            given_item: None,
363        }
364    }
365}
366
367// Represents a message passed back to rtsim from an agent's brain
368#[derive(Clone, Debug)]
369pub enum NpcInput {
370    Report(ReportId),
371    Interaction(Actor),
372    Dialogue(Actor, Dialogue<true>),
373    // TODO: Make this more principled, currently only used by pirates
374    Msg { from: Actor, msg: NpcMsg },
375}
376
377/// Represents a message that may be communicated between NPCs on a 1:1 basis
378#[derive(Clone, Debug)]
379pub enum NpcMsg {
380    /// Send by an NPC that wants to hire the receiver.
381    RequestHire,
382    /// Sent by an NPC to a hired hand when they wish to end the hiring
383    /// relationship
384    EndHire,
385}
386
387/// Abstractly represents categories of resources that might naturally appear in
388/// the world as a product of world generation.
389///
390/// Representing resources abstractly in this way allows us to decouple rtsim
391/// from the rest of the game so that we don't have to include things like
392/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`] in rtsim's
393/// persistence data model (which would require non-trivial migration work if
394/// those enums and their attributes change over time).
395///
396/// Terrain resources are usually tracked per-chunk, but this is not always
397/// true. For example, a site might contain farm fields, and those fields might
398/// track their resources independently of the chunks they appear in at some
399/// future stage of development.
400///
401/// You can determine the rtsim resource represented by a block with
402/// [`common::terrain::Block::get_rtsim_resource`].
403///
404/// Going in the other direction is necessarily a stochastic endeavour. For
405/// example, both `SpriteKind::Diamond` and `SpriteKind::Amethyst` currently map
406/// to `TerrainResource::Gem`, which is a lossy conversion: to go back the other
407/// way, we have to pick one or the other with some probability. It might be
408/// desirable to weight this probability according to the commonly accepted
409/// scarcity/value of the item to avoid balancing issues.
410///
411/// If you want to track inventory items with rtsim, see [`ItemResource`].
412// Note: the `serde(name = "...")` is to minimise the length of field
413// identifiers for the sake of rtsim persistence
414#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
415pub enum TerrainResource {
416    #[serde(rename = "0")]
417    Grass,
418    #[serde(rename = "1")]
419    Flower,
420    #[serde(rename = "2")]
421    Fruit,
422    #[serde(rename = "3")]
423    Vegetable,
424    #[serde(rename = "4")]
425    Mushroom,
426    #[serde(rename = "5")]
427    Loot, // Chests, boxes, potions, etc.
428    #[serde(rename = "6")]
429    Plant, // Flax, cotton, wheat, corn, etc.
430    #[serde(rename = "7")]
431    Stone,
432    #[serde(rename = "8")]
433    Wood, // Twigs, logs, bamboo, etc.
434    #[serde(rename = "9")]
435    Gem, // Amethyst, diamond, etc.
436    #[serde(rename = "a")]
437    Ore, // Iron, copper, etc.
438}
439
440/// Like [`TerrainResource`], but for tracking inventory items in rtsim for the
441/// sake of questing, trade, etc.
442///
443/// This type is a conceptual dual of [`TerrainResource`], so most of the same
444/// ideas apply. It is to [`common::comp::Item`] what [`TerrainResource`] is to
445/// [`common::terrain::BlockKind`] or [`common::terrain::SpriteKind`].
446///
447/// | In-game types             | Rtsim representation |
448/// |---------------------------|----------------------|
449/// | `Item`                    | `ItemResource`       |
450/// | `SpriteKind`, `BlockKind` | `TerrainResource`    |
451#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
452pub enum ItemResource {
453    #[serde(rename = "0")]
454    Coin,
455}
456
457impl ItemResource {
458    /// Attempt to translate this resource into an equivalent [`ItemDef`].
459    // TODO: Return (Arc<ItemDef>, f32) to allow for an exchange rate
460    // TODO: Have this function take an `impl Rng` so that it can be stochastic
461    pub fn to_equivalent_item_def(&self) -> Arc<ItemDef> {
462        match self {
463            Self::Coin => Arc::<ItemDef>::load_cloned("common.items.utility.coins").unwrap(),
464        }
465    }
466}
467
468// Note: the `serde(name = "...")` is to minimise the length of field
469// identifiers for the sake of rtsim persistence
470#[derive(Clone, Debug, Serialize, Deserialize)]
471pub enum Role {
472    #[serde(rename = "0")]
473    Civilised(Option<Profession>),
474    #[serde(rename = "1")]
475    Wild,
476    #[serde(rename = "2")]
477    Monster,
478    #[serde(rename = "3")]
479    Vehicle,
480}
481
482// Note: the `serde(name = "...")` is to minimise the length of field
483// identifiers for the sake of rtsim persistence
484#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
485pub enum Profession {
486    #[serde(rename = "0")]
487    Farmer,
488    #[serde(rename = "1")]
489    Hunter,
490    #[serde(rename = "2")]
491    Merchant,
492    #[serde(rename = "3")]
493    Guard,
494    #[serde(rename = "4")]
495    Adventurer(u32),
496    #[serde(rename = "5")]
497    Blacksmith,
498    #[serde(rename = "6")]
499    Chef,
500    #[serde(rename = "7")]
501    Alchemist,
502    #[serde(rename = "8")]
503    /// True if leader
504    Pirate(bool),
505    #[serde(rename = "9")]
506    Cultist,
507    #[serde(rename = "10")]
508    Herbalist,
509    #[serde(rename = "11")]
510    Captain,
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize)]
514pub struct WorldSettings {
515    pub start_time: f64,
516}
517
518impl Default for WorldSettings {
519    fn default() -> Self {
520        Self {
521            start_time: 9.0 * 3600.0, // 9am
522        }
523    }
524}