mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added report system, information sharing, made NPCs act on sentiments
This commit is contained in:
parent
08338436ea
commit
2fbddafd0a
@ -227,6 +227,8 @@ pub enum NpcAction {
|
|||||||
// TODO: Use some sort of structured, language-independent value that frontends can translate
|
// TODO: Use some sort of structured, language-independent value that frontends can translate
|
||||||
// instead
|
// instead
|
||||||
Say(Option<Actor>, Cow<'static, str>),
|
Say(Option<Actor>, Cow<'static, str>),
|
||||||
|
/// Attack the given target
|
||||||
|
Attack(Actor),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: the `serde(name = "...")` is to minimise the length of field
|
// Note: the `serde(name = "...")` is to minimise the length of field
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
data::npc::{Controller, Npc, NpcId},
|
data::{
|
||||||
|
npc::{Controller, Npc, NpcId},
|
||||||
|
ReportId, Sentiments,
|
||||||
|
},
|
||||||
RtState,
|
RtState,
|
||||||
};
|
};
|
||||||
use common::resources::{Time, TimeOfDay};
|
use common::resources::{Time, TimeOfDay};
|
||||||
|
use hashbrown::HashSet;
|
||||||
use rand_chacha::ChaChaRng;
|
use rand_chacha::ChaChaRng;
|
||||||
use std::{any::Any, marker::PhantomData, ops::ControlFlow};
|
use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow};
|
||||||
use world::{IndexRef, World};
|
use world::{IndexRef, World};
|
||||||
|
|
||||||
/// The context provided to an [`Action`] while it is being performed. It should
|
/// The context provided to an [`Action`] while it is being performed. It should
|
||||||
@ -21,6 +25,9 @@ pub struct NpcCtx<'a> {
|
|||||||
pub npc_id: NpcId,
|
pub npc_id: NpcId,
|
||||||
pub npc: &'a Npc,
|
pub npc: &'a Npc,
|
||||||
pub controller: &'a mut Controller,
|
pub controller: &'a mut Controller,
|
||||||
|
pub inbox: &'a mut VecDeque<ReportId>, // TODO: Allow more inbox items
|
||||||
|
pub sentiments: &'a mut Sentiments,
|
||||||
|
pub known_reports: &'a mut HashSet<ReportId>,
|
||||||
|
|
||||||
pub rng: ChaChaRng,
|
pub rng: ChaChaRng,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
pub use common::rtsim::{Actor, FactionId};
|
use crate::data::Sentiments;
|
||||||
|
use common::rtsim::Actor;
|
||||||
|
pub use common::rtsim::FactionId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use slotmap::HopSlotMap;
|
use slotmap::HopSlotMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
@ -6,8 +8,19 @@ use vek::*;
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Faction {
|
pub struct Faction {
|
||||||
|
pub seed: u32,
|
||||||
pub leader: Option<Actor>,
|
pub leader: Option<Actor>,
|
||||||
pub good_or_evil: bool, // TODO: Very stupid, get rid of this
|
pub good_or_evil: bool, // TODO: Very stupid, get rid of this
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub sentiments: Sentiments,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Faction {
|
||||||
|
pub fn cleanup(&mut self) {
|
||||||
|
self.sentiments
|
||||||
|
.cleanup(crate::data::sentiment::FACTION_MAX_SENTIMENTS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
pub mod faction;
|
pub mod faction;
|
||||||
pub mod nature;
|
pub mod nature;
|
||||||
pub mod npc;
|
pub mod npc;
|
||||||
|
pub mod report;
|
||||||
pub mod sentiment;
|
pub mod sentiment;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
|
|
||||||
@ -8,6 +9,8 @@ pub use self::{
|
|||||||
faction::{Faction, FactionId, Factions},
|
faction::{Faction, FactionId, Factions},
|
||||||
nature::Nature,
|
nature::Nature,
|
||||||
npc::{Npc, NpcId, Npcs},
|
npc::{Npc, NpcId, Npcs},
|
||||||
|
report::{Report, ReportId, ReportKind, Reports},
|
||||||
|
sentiment::{Sentiment, Sentiments},
|
||||||
site::{Site, SiteId, Sites},
|
site::{Site, SiteId, Sites},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +33,8 @@ pub struct Data {
|
|||||||
pub sites: Sites,
|
pub sites: Sites,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub factions: Factions,
|
pub factions: Factions,
|
||||||
|
#[serde(default)]
|
||||||
|
pub reports: Reports,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tick: u64,
|
pub tick: u64,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
use crate::{ai::Action, data::sentiment::Sentiments, gen::name};
|
use crate::{
|
||||||
|
ai::Action,
|
||||||
|
data::{ReportId, Reports, Sentiments},
|
||||||
|
gen::name,
|
||||||
|
};
|
||||||
pub use common::rtsim::{NpcId, Profession};
|
pub use common::rtsim::{NpcId, Profession};
|
||||||
use common::{
|
use common::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
@ -11,7 +15,7 @@ use common::{
|
|||||||
terrain::TerrainChunkSize,
|
terrain::TerrainChunkSize,
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::{HashMap, HashSet};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use slotmap::HopSlotMap;
|
use slotmap::HopSlotMap;
|
||||||
@ -73,6 +77,10 @@ impl Controller {
|
|||||||
pub fn say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) {
|
pub fn say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) {
|
||||||
self.actions.push(NpcAction::Say(target.into(), msg.into()));
|
self.actions.push(NpcAction::Say(target.into(), msg.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn attack(&mut self, target: impl Into<Actor>) {
|
||||||
|
self.actions.push(NpcAction::Attack(target.into()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Brain {
|
pub struct Brain {
|
||||||
@ -92,6 +100,9 @@ pub struct Npc {
|
|||||||
pub faction: Option<FactionId>,
|
pub faction: Option<FactionId>,
|
||||||
pub riding: Option<Riding>,
|
pub riding: Option<Riding>,
|
||||||
|
|
||||||
|
/// The [`Report`]s that the NPC is aware of.
|
||||||
|
pub known_reports: HashSet<ReportId>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub personality: Personality,
|
pub personality: Personality,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -105,6 +116,8 @@ pub struct Npc {
|
|||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub controller: Controller,
|
pub controller: Controller,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub inbox: VecDeque<ReportId>,
|
||||||
|
|
||||||
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
|
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
|
||||||
/// server, loaded corresponds to being within a loaded chunk). When in
|
/// server, loaded corresponds to being within a loaded chunk). When in
|
||||||
@ -126,6 +139,7 @@ impl Clone for Npc {
|
|||||||
home: self.home,
|
home: self.home,
|
||||||
faction: self.faction,
|
faction: self.faction,
|
||||||
riding: self.riding.clone(),
|
riding: self.riding.clone(),
|
||||||
|
known_reports: self.known_reports.clone(),
|
||||||
body: self.body,
|
body: self.body,
|
||||||
personality: self.personality,
|
personality: self.personality,
|
||||||
sentiments: self.sentiments.clone(),
|
sentiments: self.sentiments.clone(),
|
||||||
@ -133,6 +147,7 @@ impl Clone for Npc {
|
|||||||
chunk_pos: None,
|
chunk_pos: None,
|
||||||
current_site: Default::default(),
|
current_site: Default::default(),
|
||||||
controller: Default::default(),
|
controller: Default::default(),
|
||||||
|
inbox: Default::default(),
|
||||||
mode: Default::default(),
|
mode: Default::default(),
|
||||||
brain: Default::default(),
|
brain: Default::default(),
|
||||||
}
|
}
|
||||||
@ -148,15 +163,17 @@ impl Npc {
|
|||||||
seed,
|
seed,
|
||||||
wpos,
|
wpos,
|
||||||
body,
|
body,
|
||||||
personality: Personality::default(),
|
personality: Default::default(),
|
||||||
sentiments: Sentiments::default(),
|
sentiments: Default::default(),
|
||||||
profession: None,
|
profession: None,
|
||||||
home: None,
|
home: None,
|
||||||
faction: None,
|
faction: None,
|
||||||
riding: None,
|
riding: None,
|
||||||
|
known_reports: Default::default(),
|
||||||
chunk_pos: None,
|
chunk_pos: None,
|
||||||
current_site: None,
|
current_site: None,
|
||||||
controller: Controller::default(),
|
controller: Default::default(),
|
||||||
|
inbox: Default::default(),
|
||||||
mode: SimulationMode::Simulated,
|
mode: SimulationMode::Simulated,
|
||||||
brain: None,
|
brain: None,
|
||||||
}
|
}
|
||||||
@ -201,6 +218,16 @@ impl Npc {
|
|||||||
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) }
|
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) }
|
||||||
|
|
||||||
pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) }
|
pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) }
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self, reports: &Reports) {
|
||||||
|
// Clear old or superfluous sentiments
|
||||||
|
self.sentiments
|
||||||
|
.cleanup(crate::data::sentiment::NPC_MAX_SENTIMENTS);
|
||||||
|
// Clear reports that have been forgotten
|
||||||
|
self.known_reports
|
||||||
|
.retain(|report| reports.contains_key(*report));
|
||||||
|
// TODO: Clear old inbox items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
68
rtsim/src/data/report.rs
Normal file
68
rtsim/src/data/report.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
use common::{resources::TimeOfDay, rtsim::Actor};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use slotmap::HopSlotMap;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use vek::*;
|
||||||
|
|
||||||
|
slotmap::new_key_type! { pub struct ReportId; }
|
||||||
|
|
||||||
|
/// Represents a single piece of information known by an rtsim entity.
|
||||||
|
///
|
||||||
|
/// Reports are the medium through which rtsim represents information sharing
|
||||||
|
/// between NPCs, factions, and sites. They can represent deaths, attacks,
|
||||||
|
/// changes in diplomacy, or any other piece of information representing a
|
||||||
|
/// singular event that might be communicated.
|
||||||
|
///
|
||||||
|
/// Note that they should not be used to communicate sentiments like 'this actor
|
||||||
|
/// is friendly': the [`crate::data::Sentiment`] system should be used for that.
|
||||||
|
/// Some events might generate both a report and a change in sentiment. For
|
||||||
|
/// example, the murder of an NPC might generate both a murder report and highly
|
||||||
|
/// negative sentiments.
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Report {
|
||||||
|
pub kind: ReportKind,
|
||||||
|
pub at: TimeOfDay,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
/// The time, in in-game seconds, for which the report will be remembered
|
||||||
|
fn remember_for(&self) -> f64 {
|
||||||
|
const DAYS: f64 = 60.0 * 60.0 * 24.0;
|
||||||
|
match &self.kind {
|
||||||
|
ReportKind::Death { killer, .. } => {
|
||||||
|
if killer.is_some() {
|
||||||
|
// Murder is less easy to forget
|
||||||
|
DAYS * 15.0
|
||||||
|
} else {
|
||||||
|
DAYS * 5.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ReportKind {
|
||||||
|
Death { actor: Actor, killer: Option<Actor> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Reports {
|
||||||
|
pub reports: HopSlotMap<ReportId, Report>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reports {
|
||||||
|
pub fn create(&mut self, report: Report) -> ReportId { self.reports.insert(report) }
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self, current_time: TimeOfDay) {
|
||||||
|
// Forget reports that are too old
|
||||||
|
self.reports
|
||||||
|
.retain(|_, report| (current_time.0 - report.at.0).max(0.0) < report.remember_for());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Reports {
|
||||||
|
type Target = HopSlotMap<ReportId, Report>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target { &self.reports }
|
||||||
|
}
|
@ -2,15 +2,18 @@ use common::{
|
|||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
rtsim::{Actor, FactionId, NpcId},
|
rtsim::{Actor, FactionId, NpcId},
|
||||||
};
|
};
|
||||||
|
use hashbrown::HashMap;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// Factions have a larger 'social memory' than individual NPCs and so we allow
|
// Factions have a larger 'social memory' than individual NPCs and so we allow
|
||||||
// them to have more sentiments
|
// them to have more sentiments
|
||||||
pub const FACTION_MAX_SENTIMENTS: usize = 1024;
|
pub const FACTION_MAX_SENTIMENTS: usize = 1024;
|
||||||
pub const NPC_MAX_SENTIMENTS: usize = 128;
|
pub const NPC_MAX_SENTIMENTS: usize = 128;
|
||||||
|
|
||||||
|
// Magic factor used to control sentiment decay speed
|
||||||
|
const DECAY_FACTOR: f32 = 6.0;
|
||||||
|
|
||||||
/// The target that a sentiment is felt toward.
|
/// The target that a sentiment is felt toward.
|
||||||
// NOTE: More could be added to this! For example:
|
// NOTE: More could be added to this! For example:
|
||||||
// - Animal species (dislikes spiders?)
|
// - Animal species (dislikes spiders?)
|
||||||
@ -55,39 +58,39 @@ impl Sentiments {
|
|||||||
self.map.get(&target.into()).copied().unwrap_or_default()
|
self.map.get(&target.into()).copied().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_by(&mut self, target: impl Into<Target>, change: f32) {
|
/// Change the sentiment toward the given target by the given amount,
|
||||||
|
/// capping out at the given value.
|
||||||
|
pub fn change_by(&mut self, target: impl Into<Target>, change: f32, cap: f32) {
|
||||||
let target = target.into();
|
let target = target.into();
|
||||||
self.map.entry(target).or_default().change_by(change);
|
self.map.entry(target).or_default().change_by(change, cap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Progressively decay the sentiment back to a neutral sentiment.
|
/// Progressively decay the sentiment back to a neutral sentiment.
|
||||||
///
|
///
|
||||||
/// Note that sentiment get decay gets slower the harsher the sentiment is.
|
/// Note that sentiment get decay gets slower the harsher the sentiment is.
|
||||||
/// You can calculate the **average** number of ticks required for a
|
/// You can calculate the **average** number of seconds required for a
|
||||||
/// sentiment to decay with the following formula:
|
/// sentiment to neutral decay with the following formula:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// ticks_until_neutrality = ((sentiment_value * 127 * 32) ^ 2) / 2
|
/// seconds_until_neutrality = ((sentiment_value * 127 * DECAY_FACTOR) ^ 2) / 2
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// For example, a positive (see [`Sentiment::POSITIVE`]) sentiment has a
|
/// For example, a positive (see [`Sentiment::POSITIVE`]) sentiment has a
|
||||||
/// value of `0.2`, so we get
|
/// value of `0.2`, so we get
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// ticks_until_neutrality = ((0.1 * 127 * 32) ^ 2) / 2 = ~82,580 ticks
|
/// seconds_until_neutrality = ((0.1 * 127 * DECAY_FACTOR) ^ 2) / 2 = ~2,903 seconds, or 48 minutes
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Assuming a TPS of 30, that's ~46 minutes.
|
|
||||||
///
|
|
||||||
/// Some 'common' sentiment decay times are as follows:
|
/// Some 'common' sentiment decay times are as follows:
|
||||||
///
|
///
|
||||||
/// - `POSITIVE`/`NEGATIVE`: ~46 minutes
|
/// - `POSITIVE`/`NEGATIVE`: ~48 minutes
|
||||||
/// - `ALLY`/`RIVAL`: ~6.9 hours
|
/// - `ALLY`/`RIVAL`: ~7 hours
|
||||||
/// - `FRIEND`/`ENEMY`: ~27.5 hours
|
/// - `FRIEND`/`ENEMY`: ~29 hours
|
||||||
/// - `HERO`/`VILLAIN`: ~48.9 hours
|
/// - `HERO`/`VILLAIN`: ~65 hours
|
||||||
pub fn decay(&mut self, rng: &mut impl Rng) {
|
pub fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
|
||||||
self.map.retain(|_, sentiment| {
|
self.map.retain(|_, sentiment| {
|
||||||
sentiment.decay(rng);
|
sentiment.decay(rng, dt);
|
||||||
// We can eliminate redundant sentiments that don't need remembering
|
// We can eliminate redundant sentiments that don't need remembering
|
||||||
!sentiment.is_redundant()
|
!sentiment.is_redundant()
|
||||||
});
|
});
|
||||||
@ -152,26 +155,30 @@ impl Sentiment {
|
|||||||
/// generally try to harm the actor in any way they can.
|
/// generally try to harm the actor in any way they can.
|
||||||
pub const VILLAIN: f32 = -0.8;
|
pub const VILLAIN: f32 = -0.8;
|
||||||
|
|
||||||
fn value(&self) -> f32 { self.positivity as f32 / 127.0 }
|
fn value(&self) -> f32 { self.positivity as f32 / 126.0 }
|
||||||
|
|
||||||
fn change_by(&mut self, change: f32) {
|
fn change_by(&mut self, change: f32, cap: f32) {
|
||||||
// There's a bit of ceremony here for two reasons:
|
// There's a bit of ceremony here for two reasons:
|
||||||
// 1) Very small changes should not be rounded to 0
|
// 1) Very small changes should not be rounded to 0
|
||||||
// 2) Sentiment should never (over/under)flow
|
// 2) Sentiment should never (over/under)flow
|
||||||
if change != 0.0 {
|
if change != 0.0 {
|
||||||
let abs = (change * 127.0).abs().clamp(1.0, 127.0) as i8;
|
let abs = (change * 126.0).abs().clamp(1.0, 126.0) as i8;
|
||||||
|
let cap = (cap.abs().min(1.0) * 126.0) as i8;
|
||||||
self.positivity = if change > 0.0 {
|
self.positivity = if change > 0.0 {
|
||||||
self.positivity.saturating_add(abs)
|
self.positivity.saturating_add(abs).min(cap)
|
||||||
} else {
|
} else {
|
||||||
self.positivity.saturating_sub(abs)
|
self.positivity.saturating_sub(abs).max(-cap)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decay(&mut self, rng: &mut impl Rng) {
|
fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
|
||||||
if self.positivity != 0 {
|
if self.positivity != 0 {
|
||||||
// TODO: Make dt-independent so we can slow tick rates
|
// TODO: Make dt-independent so we can slow tick rates
|
||||||
if rng.gen_range(0..self.positivity.unsigned_abs() as u32 * 1024) == 0 {
|
// 36 = 6 * 6
|
||||||
|
if rng.gen_bool(
|
||||||
|
(1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_FACTOR.powi(2) * dt)) as f64,
|
||||||
|
) {
|
||||||
self.positivity -= self.positivity.signum();
|
self.positivity -= self.positivity.signum();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::data::{ReportId, Reports};
|
||||||
pub use common::rtsim::SiteId;
|
pub use common::rtsim::SiteId;
|
||||||
use common::{
|
use common::{
|
||||||
rtsim::{FactionId, NpcId},
|
rtsim::{FactionId, NpcId},
|
||||||
@ -12,9 +13,14 @@ use world::site::Site as WorldSite;
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Site {
|
pub struct Site {
|
||||||
|
pub seed: u32,
|
||||||
pub wpos: Vec2<i32>,
|
pub wpos: Vec2<i32>,
|
||||||
pub faction: Option<FactionId>,
|
pub faction: Option<FactionId>,
|
||||||
|
|
||||||
|
/// The [`Report`]s that the site tracks (you can imagine them being on a
|
||||||
|
/// noticeboard or something).
|
||||||
|
pub known_reports: HashSet<ReportId>,
|
||||||
|
|
||||||
/// The site generated during initial worldgen that this site corresponds
|
/// The site generated during initial worldgen that this site corresponds
|
||||||
/// to.
|
/// to.
|
||||||
///
|
///
|
||||||
@ -40,6 +46,12 @@ impl Site {
|
|||||||
self.faction = faction.into();
|
self.faction = faction.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self, reports: &Reports) {
|
||||||
|
// Clear reports that have been forgotten
|
||||||
|
self.known_reports
|
||||||
|
.retain(|report| reports.contains_key(*report));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
|
@ -3,6 +3,7 @@ use common::{
|
|||||||
resources::{Time, TimeOfDay},
|
resources::{Time, TimeOfDay},
|
||||||
rtsim::Actor,
|
rtsim::Actor,
|
||||||
};
|
};
|
||||||
|
use vek::*;
|
||||||
use world::{IndexRef, World};
|
use world::{IndexRef, World};
|
||||||
|
|
||||||
pub trait Event: Clone + 'static {}
|
pub trait Event: Clone + 'static {}
|
||||||
@ -31,6 +32,7 @@ impl Event for OnTick {}
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OnDeath {
|
pub struct OnDeath {
|
||||||
pub actor: Actor,
|
pub actor: Actor,
|
||||||
|
pub wpos: Option<Vec3<f32>>,
|
||||||
pub killer: Option<Actor>,
|
pub killer: Option<Actor>,
|
||||||
}
|
}
|
||||||
impl Event for OnDeath {}
|
impl Event for OnDeath {}
|
||||||
|
@ -5,8 +5,10 @@ use world::{IndexRef, World};
|
|||||||
impl Faction {
|
impl Faction {
|
||||||
pub fn generate(_world: &World, _index: IndexRef, rng: &mut impl Rng) -> Self {
|
pub fn generate(_world: &World, _index: IndexRef, rng: &mut impl Rng) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
seed: rng.gen(),
|
||||||
leader: None,
|
leader: None,
|
||||||
good_or_evil: rng.gen(),
|
good_or_evil: rng.gen(),
|
||||||
|
sentiments: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ pub mod name;
|
|||||||
pub mod site;
|
pub mod site;
|
||||||
|
|
||||||
use crate::data::{
|
use crate::data::{
|
||||||
faction::{Faction, Factions},
|
faction::Faction,
|
||||||
npc::{Npc, Npcs, Profession, Vehicle},
|
npc::{Npc, Npcs, Profession, Vehicle},
|
||||||
site::{Site, Sites},
|
site::Site,
|
||||||
Data, Nature,
|
Data, Nature,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
@ -37,13 +37,9 @@ impl Data {
|
|||||||
npc_grid: Grid::new(Vec2::zero(), Default::default()),
|
npc_grid: Grid::new(Vec2::zero(), Default::default()),
|
||||||
character_map: Default::default(),
|
character_map: Default::default(),
|
||||||
},
|
},
|
||||||
sites: Sites {
|
sites: Default::default(),
|
||||||
sites: Default::default(),
|
factions: Default::default(),
|
||||||
world_site_map: Default::default(),
|
reports: Default::default(),
|
||||||
},
|
|
||||||
factions: Factions {
|
|
||||||
factions: Default::default(),
|
|
||||||
},
|
|
||||||
|
|
||||||
tick: 0,
|
tick: 0,
|
||||||
time_of_day: TimeOfDay(settings.start_time),
|
time_of_day: TimeOfDay(settings.start_time),
|
||||||
@ -72,6 +68,7 @@ impl Data {
|
|||||||
index,
|
index,
|
||||||
&initial_factions,
|
&initial_factions,
|
||||||
&this.factions,
|
&this.factions,
|
||||||
|
&mut rng,
|
||||||
);
|
);
|
||||||
this.sites.create(site);
|
this.sites.create(site);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::data::{FactionId, Factions, Site};
|
use crate::data::{FactionId, Factions, Site};
|
||||||
use common::store::Id;
|
use common::store::Id;
|
||||||
use hashbrown::HashSet;
|
use rand::prelude::*;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use world::{
|
use world::{
|
||||||
site::{Site as WorldSite, SiteKind},
|
site::{Site as WorldSite, SiteKind},
|
||||||
@ -14,6 +14,7 @@ impl Site {
|
|||||||
index: IndexRef,
|
index: IndexRef,
|
||||||
nearby_factions: &[(Vec2<i32>, FactionId)],
|
nearby_factions: &[(Vec2<i32>, FactionId)],
|
||||||
factions: &Factions,
|
factions: &Factions,
|
||||||
|
rng: &mut impl Rng,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let world_site = index.sites.get(world_site_id);
|
let world_site = index.sites.get(world_site_id);
|
||||||
let wpos = world_site.get_origin();
|
let wpos = world_site.get_origin();
|
||||||
@ -36,6 +37,7 @@ impl Site {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
seed: rng.gen(),
|
||||||
wpos,
|
wpos,
|
||||||
world_site: Some(world_site_id),
|
world_site: Some(world_site_id),
|
||||||
faction: good_or_evil.and_then(|good_or_evil| {
|
faction: good_or_evil.and_then(|good_or_evil| {
|
||||||
@ -49,7 +51,8 @@ impl Site {
|
|||||||
.min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
|
.min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
|
||||||
.map(|(_, faction)| *faction)
|
.map(|(_, faction)| *faction)
|
||||||
}),
|
}),
|
||||||
population: HashSet::new(),
|
population: Default::default(),
|
||||||
|
known_reports: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,9 +62,11 @@ impl RtState {
|
|||||||
info!("Starting default rtsim rules...");
|
info!("Starting default rtsim rules...");
|
||||||
self.start_rule::<rule::migrate::Migrate>();
|
self.start_rule::<rule::migrate::Migrate>();
|
||||||
self.start_rule::<rule::replenish_resources::ReplenishResources>();
|
self.start_rule::<rule::replenish_resources::ReplenishResources>();
|
||||||
|
self.start_rule::<rule::report::ReportEvents>();
|
||||||
self.start_rule::<rule::sync_npcs::SyncNpcs>();
|
self.start_rule::<rule::sync_npcs::SyncNpcs>();
|
||||||
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
|
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
|
||||||
self.start_rule::<rule::npc_ai::NpcAi>();
|
self.start_rule::<rule::npc_ai::NpcAi>();
|
||||||
|
self.start_rule::<rule::cleanup::CleanUp>();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_rule<R: Rule>(&mut self) {
|
pub fn start_rule<R: Rule>(&mut self) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
pub mod cleanup;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
pub mod npc_ai;
|
pub mod npc_ai;
|
||||||
pub mod replenish_resources;
|
pub mod replenish_resources;
|
||||||
|
pub mod report;
|
||||||
pub mod simulate_npcs;
|
pub mod simulate_npcs;
|
||||||
pub mod sync_npcs;
|
pub mod sync_npcs;
|
||||||
|
|
||||||
|
55
rtsim/src/rule/cleanup.rs
Normal file
55
rtsim/src/rule/cleanup.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use crate::{event::OnTick, RtState, Rule, RuleError};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand_chacha::ChaChaRng;
|
||||||
|
|
||||||
|
/// Prevent performing cleanup for every NPC every tick
|
||||||
|
const NPC_SENTIMENT_TICK_SKIP: u64 = 30;
|
||||||
|
const NPC_CLEANUP_TICK_SKIP: u64 = 100;
|
||||||
|
const FACTION_CLEANUP_TICK_SKIP: u64 = 30;
|
||||||
|
const SITE_CLEANUP_TICK_SKIP: u64 = 30;
|
||||||
|
|
||||||
|
/// A rule that cleans up data structures in rtsim: removing old reports,
|
||||||
|
/// irrelevant sentiments, etc.
|
||||||
|
///
|
||||||
|
/// Also performs sentiment decay (although this should be moved elsewhere)
|
||||||
|
pub struct CleanUp;
|
||||||
|
|
||||||
|
impl Rule for CleanUp {
|
||||||
|
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||||
|
rtstate.bind::<Self, OnTick>(|ctx| {
|
||||||
|
let data = &mut *ctx.state.data_mut();
|
||||||
|
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
||||||
|
|
||||||
|
for (_, npc) in data.npcs
|
||||||
|
.iter_mut()
|
||||||
|
// Only cleanup NPCs every few ticks
|
||||||
|
.filter(|(_, npc)| (npc.seed as u64 + ctx.event.tick) % NPC_SENTIMENT_TICK_SKIP == 0)
|
||||||
|
{
|
||||||
|
npc.sentiments.decay(&mut rng, ctx.event.dt * NPC_SENTIMENT_TICK_SKIP as f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up entities
|
||||||
|
data.npcs
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(_, npc)| (npc.seed as u64 + ctx.event.tick) % NPC_CLEANUP_TICK_SKIP == 0)
|
||||||
|
.for_each(|(_, npc)| npc.cleanup(&data.reports));
|
||||||
|
|
||||||
|
// Clean up factions
|
||||||
|
data.factions
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(_, faction)| (faction.seed as u64 + ctx.event.tick) % FACTION_CLEANUP_TICK_SKIP == 0)
|
||||||
|
.for_each(|(_, faction)| faction.cleanup());
|
||||||
|
|
||||||
|
// Clean up sites
|
||||||
|
data.sites
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(_, site)| (site.seed as u64 + ctx.event.tick) % SITE_CLEANUP_TICK_SKIP == 0)
|
||||||
|
.for_each(|(_, site)| site.cleanup(&data.reports));
|
||||||
|
|
||||||
|
// Clean up old reports
|
||||||
|
data.reports.cleanup(data.time_of_day);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
use crate::{data::Site, event::OnSetup, RtState, Rule, RuleError};
|
use crate::{data::Site, event::OnSetup, RtState, Rule, RuleError};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand_chacha::ChaChaRng;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
/// This rule runs at rtsim startup and broadly acts to perform some primitive
|
/// This rule runs at rtsim startup and broadly acts to perform some primitive
|
||||||
@ -10,6 +12,9 @@ impl Rule for Migrate {
|
|||||||
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||||
rtstate.bind::<Self, OnSetup>(|ctx| {
|
rtstate.bind::<Self, OnSetup>(|ctx| {
|
||||||
let data = &mut *ctx.state.data_mut();
|
let data = &mut *ctx.state.data_mut();
|
||||||
|
|
||||||
|
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
||||||
|
|
||||||
// Delete rtsim sites that don't correspond to a world site
|
// Delete rtsim sites that don't correspond to a world site
|
||||||
data.sites.sites.retain(|site_id, site| {
|
data.sites.sites.retain(|site_id, site| {
|
||||||
if let Some((world_site_id, _)) = ctx
|
if let Some((world_site_id, _)) = ctx
|
||||||
@ -55,6 +60,7 @@ impl Rule for Migrate {
|
|||||||
ctx.index,
|
ctx.index,
|
||||||
&[],
|
&[],
|
||||||
&data.factions,
|
&data.factions,
|
||||||
|
&mut rng,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
|
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
|
||||||
data::{
|
data::{
|
||||||
npc::{Brain, PathData, SimulationMode},
|
npc::{Brain, PathData, SimulationMode},
|
||||||
Sites,
|
ReportKind, Sentiment, Sites,
|
||||||
},
|
},
|
||||||
event::OnTick,
|
event::OnTick,
|
||||||
RtState, Rule, RuleError,
|
RtState, Rule, RuleError,
|
||||||
@ -219,10 +219,13 @@ impl Rule for NpcAi {
|
|||||||
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
|
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
|
||||||
.map(|(npc_id, npc)| {
|
.map(|(npc_id, npc)| {
|
||||||
let controller = std::mem::take(&mut npc.controller);
|
let controller = std::mem::take(&mut npc.controller);
|
||||||
|
let inbox = std::mem::take(&mut npc.inbox);
|
||||||
|
let sentiments = std::mem::take(&mut npc.sentiments);
|
||||||
|
let known_reports = std::mem::take(&mut npc.known_reports);
|
||||||
let brain = npc.brain.take().unwrap_or_else(|| Brain {
|
let brain = npc.brain.take().unwrap_or_else(|| Brain {
|
||||||
action: Box::new(think().repeat()),
|
action: Box::new(think().repeat()),
|
||||||
});
|
});
|
||||||
(npc_id, controller, brain)
|
(npc_id, controller, inbox, sentiments, known_reports, brain)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
@ -233,7 +236,7 @@ impl Rule for NpcAi {
|
|||||||
|
|
||||||
npc_data
|
npc_data
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(|(npc_id, controller, brain)| {
|
.for_each(|(npc_id, controller, inbox, sentiments, known_reports, brain)| {
|
||||||
let npc = &data.npcs[*npc_id];
|
let npc = &data.npcs[*npc_id];
|
||||||
|
|
||||||
brain.action.tick(&mut NpcCtx {
|
brain.action.tick(&mut NpcCtx {
|
||||||
@ -245,6 +248,9 @@ impl Rule for NpcAi {
|
|||||||
npc,
|
npc,
|
||||||
npc_id: *npc_id,
|
npc_id: *npc_id,
|
||||||
controller,
|
controller,
|
||||||
|
inbox,
|
||||||
|
known_reports,
|
||||||
|
sentiments,
|
||||||
rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
|
rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -252,9 +258,12 @@ impl Rule for NpcAi {
|
|||||||
|
|
||||||
// Reinsert NPC brains
|
// Reinsert NPC brains
|
||||||
let mut data = ctx.state.data_mut();
|
let mut data = ctx.state.data_mut();
|
||||||
for (npc_id, controller, brain) in npc_data {
|
for (npc_id, controller, inbox, sentiments, known_reports, brain) in npc_data {
|
||||||
data.npcs[npc_id].controller = controller;
|
data.npcs[npc_id].controller = controller;
|
||||||
data.npcs[npc_id].brain = Some(brain);
|
data.npcs[npc_id].brain = Some(brain);
|
||||||
|
data.npcs[npc_id].inbox = inbox;
|
||||||
|
data.npcs[npc_id].sentiments = sentiments;
|
||||||
|
data.npcs[npc_id].known_reports = known_reports;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -871,6 +880,50 @@ fn captain() -> impl Action {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
|
||||||
|
loop {
|
||||||
|
match ctx.inbox.pop_front() {
|
||||||
|
Some(report_id) if !ctx.known_reports.contains(&report_id) => {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match ctx.state.data().reports.get(report_id).map(|r| r.kind) {
|
||||||
|
Some(ReportKind::Death { killer, .. }) => {
|
||||||
|
// TODO: Sentiment should be positive if we didn't like actor that died
|
||||||
|
// TODO: Don't report self
|
||||||
|
let phrases = if let Some(killer) = killer {
|
||||||
|
// TODO: Don't hard-code sentiment change
|
||||||
|
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
|
||||||
|
&["Murderer!", "How could you do this?", "Aaargh!"][..]
|
||||||
|
} else {
|
||||||
|
&["No!", "This is terrible!", "Oh my goodness!"][..]
|
||||||
|
};
|
||||||
|
let phrase = *phrases.iter().choose(&mut ctx.rng).unwrap(); // Can't fail
|
||||||
|
ctx.known_reports.insert(report_id);
|
||||||
|
break Some(just(move |ctx| ctx.controller.say(killer, phrase)));
|
||||||
|
},
|
||||||
|
None => {}, // Stale report, ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(_) => {}, // Reports we already know of are ignored
|
||||||
|
None => break None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_for_enemies(ctx: &mut NpcCtx) -> Option<impl Action> {
|
||||||
|
ctx.state
|
||||||
|
.data()
|
||||||
|
.npcs
|
||||||
|
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 24.0)
|
||||||
|
.find(|actor| ctx.sentiments.toward(*actor).is(Sentiment::ENEMY))
|
||||||
|
.map(|enemy| just(move |ctx| ctx.controller.attack(enemy)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn react_to_events(ctx: &mut NpcCtx) -> Option<impl Action> {
|
||||||
|
check_inbox(ctx)
|
||||||
|
.map(|action| action.boxed())
|
||||||
|
.or_else(|| check_for_enemies(ctx).map(|action| action.boxed()))
|
||||||
|
}
|
||||||
|
|
||||||
fn humanoid() -> impl Action {
|
fn humanoid() -> impl Action {
|
||||||
choose(|ctx| {
|
choose(|ctx| {
|
||||||
if let Some(riding) = &ctx.npc.riding {
|
if let Some(riding) = &ctx.npc.riding {
|
||||||
@ -890,15 +943,19 @@ fn humanoid() -> impl Action {
|
|||||||
} else {
|
} else {
|
||||||
important(socialize())
|
important(socialize())
|
||||||
}
|
}
|
||||||
} else if matches!(
|
|
||||||
ctx.npc.profession,
|
|
||||||
Some(Profession::Adventurer(_) | Profession::Merchant)
|
|
||||||
) {
|
|
||||||
casual(adventure())
|
|
||||||
} else if let Some(home) = ctx.npc.home {
|
|
||||||
casual(villager(home))
|
|
||||||
} else {
|
} else {
|
||||||
casual(finish()) // Homeless
|
let action = if matches!(
|
||||||
|
ctx.npc.profession,
|
||||||
|
Some(Profession::Adventurer(_) | Profession::Merchant)
|
||||||
|
) {
|
||||||
|
adventure().boxed()
|
||||||
|
} else if let Some(home) = ctx.npc.home {
|
||||||
|
villager(home).boxed()
|
||||||
|
} else {
|
||||||
|
idle().boxed() // Homeless
|
||||||
|
};
|
||||||
|
|
||||||
|
casual(action.interrupt_with(react_to_events))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
43
rtsim/src/rule/report.rs
Normal file
43
rtsim/src/rule/report.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use crate::{
|
||||||
|
data::{report::ReportKind, Report},
|
||||||
|
event::{EventCtx, OnDeath},
|
||||||
|
RtState, Rule, RuleError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ReportEvents;
|
||||||
|
|
||||||
|
impl Rule for ReportEvents {
|
||||||
|
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||||
|
rtstate.bind::<Self, OnDeath>(on_death);
|
||||||
|
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
|
||||||
|
let data = &mut *ctx.state.data_mut();
|
||||||
|
|
||||||
|
if let Some(wpos) = ctx.event.wpos {
|
||||||
|
let nearby = data
|
||||||
|
.npcs
|
||||||
|
.nearby(None, wpos, 32.0)
|
||||||
|
.filter_map(|actor| actor.npc())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !nearby.is_empty() {
|
||||||
|
let report = data.reports.create(Report {
|
||||||
|
kind: ReportKind::Death {
|
||||||
|
actor: ctx.event.actor,
|
||||||
|
killer: ctx.event.killer,
|
||||||
|
},
|
||||||
|
at: data.time_of_day,
|
||||||
|
});
|
||||||
|
|
||||||
|
for npc_id in nearby {
|
||||||
|
if let Some(npc) = data.npcs.get_mut(npc_id) {
|
||||||
|
npc.inbox.push_back(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -246,6 +246,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
|||||||
for action in std::mem::take(&mut npc.controller.actions) {
|
for action in std::mem::take(&mut npc.controller.actions) {
|
||||||
match action {
|
match action {
|
||||||
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
|
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
|
||||||
|
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +90,23 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
|
|||||||
.find_map(|site| data.sites.world_site_map.get(site).copied())
|
.find_map(|site| data.sites.world_site_map.get(site).copied())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Share known reports with current site, if it's our home
|
||||||
|
// TODO: Only share new reports
|
||||||
|
if let Some(current_site) = npc.current_site
|
||||||
|
&& Some(current_site) == npc.home
|
||||||
|
{
|
||||||
|
if let Some(site) = data.sites.get_mut(current_site) {
|
||||||
|
// TODO: Sites should have an inbox and their own AI code
|
||||||
|
site.known_reports.extend(npc.known_reports
|
||||||
|
.iter()
|
||||||
|
.copied());
|
||||||
|
npc.inbox.extend(site.known_reports
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|report| !npc.known_reports.contains(report)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the NPC's grid cell
|
// Update the NPC's grid cell
|
||||||
let chunk_pos = npc
|
let chunk_pos = npc
|
||||||
.wpos
|
.wpos
|
||||||
|
@ -2069,13 +2069,20 @@ fn handle_kill_npcs(
|
|||||||
let to_kill = {
|
let to_kill = {
|
||||||
let ecs = server.state.ecs();
|
let ecs = server.state.ecs();
|
||||||
let entities = ecs.entities();
|
let entities = ecs.entities();
|
||||||
|
let positions = ecs.write_storage::<comp::Pos>();
|
||||||
let healths = ecs.write_storage::<comp::Health>();
|
let healths = ecs.write_storage::<comp::Health>();
|
||||||
let players = ecs.read_storage::<comp::Player>();
|
let players = ecs.read_storage::<comp::Player>();
|
||||||
let alignments = ecs.read_storage::<Alignment>();
|
let alignments = ecs.read_storage::<Alignment>();
|
||||||
|
|
||||||
(&entities, &healths, !&players, alignments.maybe())
|
(
|
||||||
|
&entities,
|
||||||
|
&healths,
|
||||||
|
!&players,
|
||||||
|
alignments.maybe(),
|
||||||
|
&positions,
|
||||||
|
)
|
||||||
.join()
|
.join()
|
||||||
.filter_map(|(entity, _health, (), alignment)| {
|
.filter_map(|(entity, _health, (), alignment, pos)| {
|
||||||
let should_kill = kill_pets
|
let should_kill = kill_pets
|
||||||
|| if let Some(Alignment::Owned(owned)) = alignment {
|
|| if let Some(Alignment::Owned(owned)) = alignment {
|
||||||
ecs.entity_from_uid(owned.0)
|
ecs.entity_from_uid(owned.0)
|
||||||
@ -2095,6 +2102,7 @@ fn handle_kill_npcs(
|
|||||||
&ecs.read_resource::<Arc<world::World>>(),
|
&ecs.read_resource::<Arc<world::World>>(),
|
||||||
ecs.read_resource::<world::IndexOwned>().as_index_ref(),
|
ecs.read_resource::<world::IndexOwned>().as_index_ref(),
|
||||||
Actor::Npc(rtsim_entity.0),
|
Actor::Npc(rtsim_entity.0),
|
||||||
|
Some(pos.0),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -529,6 +529,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
|||||||
.read_resource::<world::IndexOwned>()
|
.read_resource::<world::IndexOwned>()
|
||||||
.as_index_ref(),
|
.as_index_ref(),
|
||||||
actor,
|
actor,
|
||||||
|
state.ecs().read_storage::<Pos>().get(entity).map(|p| p.0),
|
||||||
last_change
|
last_change
|
||||||
.by
|
.by
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -62,13 +62,14 @@ impl RtSim {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Rtsim data failed to load: {}", e);
|
error!("Rtsim data failed to load: {}", e);
|
||||||
|
info!("Old rtsim data will now be moved to a backup file");
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
loop {
|
loop {
|
||||||
let mut backup_path = file_path.clone();
|
let mut backup_path = file_path.clone();
|
||||||
backup_path.set_extension(if i == 0 {
|
backup_path.set_extension(if i == 0 {
|
||||||
format!("backup_{}", i)
|
|
||||||
} else {
|
|
||||||
"ron_backup".to_string()
|
"ron_backup".to_string()
|
||||||
|
} else {
|
||||||
|
format!("ron_backup_{}", i)
|
||||||
});
|
});
|
||||||
if !backup_path.exists() {
|
if !backup_path.exists() {
|
||||||
fs::rename(&file_path, &backup_path)?;
|
fs::rename(&file_path, &backup_path)?;
|
||||||
@ -78,6 +79,11 @@ impl RtSim {
|
|||||||
);
|
);
|
||||||
info!("A fresh rtsim data will now be generated.");
|
info!("A fresh rtsim data will now be generated.");
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"Backup file {} already exists, trying another name...",
|
||||||
|
backup_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
@ -169,9 +175,18 @@ impl RtSim {
|
|||||||
world: &World,
|
world: &World,
|
||||||
index: IndexRef,
|
index: IndexRef,
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
|
wpos: Option<Vec3<f32>>,
|
||||||
killer: Option<Actor>,
|
killer: Option<Actor>,
|
||||||
) {
|
) {
|
||||||
self.state.emit(OnDeath { actor, killer }, world, index);
|
self.state.emit(
|
||||||
|
OnDeath {
|
||||||
|
wpos,
|
||||||
|
actor,
|
||||||
|
killer,
|
||||||
|
},
|
||||||
|
world,
|
||||||
|
index,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) {
|
pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) {
|
||||||
|
@ -501,6 +501,18 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
|||||||
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
NpcAction::Attack(target) => {
|
||||||
|
if let Some(target) = bdata.read_data.lookup_actor(target) {
|
||||||
|
bdata.agent.target = Some(Target::new(
|
||||||
|
target,
|
||||||
|
true,
|
||||||
|
bdata.read_data.time.0,
|
||||||
|
false,
|
||||||
|
bdata.read_data.positions.get(target).map(|p| p.0),
|
||||||
|
));
|
||||||
|
bdata.agent.awareness.set_maximally_aware();
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user