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
|
||||
// instead
|
||||
Say(Option<Actor>, Cow<'static, str>),
|
||||
/// Attack the given target
|
||||
Attack(Actor),
|
||||
}
|
||||
|
||||
// Note: the `serde(name = "...")` is to minimise the length of field
|
||||
|
@ -1,10 +1,14 @@
|
||||
use crate::{
|
||||
data::npc::{Controller, Npc, NpcId},
|
||||
data::{
|
||||
npc::{Controller, Npc, NpcId},
|
||||
ReportId, Sentiments,
|
||||
},
|
||||
RtState,
|
||||
};
|
||||
use common::resources::{Time, TimeOfDay};
|
||||
use hashbrown::HashSet;
|
||||
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};
|
||||
|
||||
/// 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: &'a Npc,
|
||||
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,
|
||||
}
|
||||
|
@ -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 slotmap::HopSlotMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
@ -6,8 +8,19 @@ use vek::*;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Faction {
|
||||
pub seed: u32,
|
||||
pub leader: Option<Actor>,
|
||||
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)]
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod faction;
|
||||
pub mod nature;
|
||||
pub mod npc;
|
||||
pub mod report;
|
||||
pub mod sentiment;
|
||||
pub mod site;
|
||||
|
||||
@ -8,6 +9,8 @@ pub use self::{
|
||||
faction::{Faction, FactionId, Factions},
|
||||
nature::Nature,
|
||||
npc::{Npc, NpcId, Npcs},
|
||||
report::{Report, ReportId, ReportKind, Reports},
|
||||
sentiment::{Sentiment, Sentiments},
|
||||
site::{Site, SiteId, Sites},
|
||||
};
|
||||
|
||||
@ -30,6 +33,8 @@ pub struct Data {
|
||||
pub sites: Sites,
|
||||
#[serde(default)]
|
||||
pub factions: Factions,
|
||||
#[serde(default)]
|
||||
pub reports: Reports,
|
||||
|
||||
#[serde(default)]
|
||||
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};
|
||||
use common::{
|
||||
character::CharacterId,
|
||||
@ -11,7 +15,7 @@ use common::{
|
||||
terrain::TerrainChunkSize,
|
||||
vol::RectVolSize,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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>>) {
|
||||
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 {
|
||||
@ -92,6 +100,9 @@ pub struct Npc {
|
||||
pub faction: Option<FactionId>,
|
||||
pub riding: Option<Riding>,
|
||||
|
||||
/// The [`Report`]s that the NPC is aware of.
|
||||
pub known_reports: HashSet<ReportId>,
|
||||
|
||||
#[serde(default)]
|
||||
pub personality: Personality,
|
||||
#[serde(default)]
|
||||
@ -105,6 +116,8 @@ pub struct Npc {
|
||||
|
||||
#[serde(skip)]
|
||||
pub controller: Controller,
|
||||
#[serde(skip)]
|
||||
pub inbox: VecDeque<ReportId>,
|
||||
|
||||
/// 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
|
||||
@ -126,6 +139,7 @@ impl Clone for Npc {
|
||||
home: self.home,
|
||||
faction: self.faction,
|
||||
riding: self.riding.clone(),
|
||||
known_reports: self.known_reports.clone(),
|
||||
body: self.body,
|
||||
personality: self.personality,
|
||||
sentiments: self.sentiments.clone(),
|
||||
@ -133,6 +147,7 @@ impl Clone for Npc {
|
||||
chunk_pos: None,
|
||||
current_site: Default::default(),
|
||||
controller: Default::default(),
|
||||
inbox: Default::default(),
|
||||
mode: Default::default(),
|
||||
brain: Default::default(),
|
||||
}
|
||||
@ -148,15 +163,17 @@ impl Npc {
|
||||
seed,
|
||||
wpos,
|
||||
body,
|
||||
personality: Personality::default(),
|
||||
sentiments: Sentiments::default(),
|
||||
personality: Default::default(),
|
||||
sentiments: Default::default(),
|
||||
profession: None,
|
||||
home: None,
|
||||
faction: None,
|
||||
riding: None,
|
||||
known_reports: Default::default(),
|
||||
chunk_pos: None,
|
||||
current_site: None,
|
||||
controller: Controller::default(),
|
||||
controller: Default::default(),
|
||||
inbox: Default::default(),
|
||||
mode: SimulationMode::Simulated,
|
||||
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 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)]
|
||||
|
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,
|
||||
rtsim::{Actor, FactionId, NpcId},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Factions have a larger 'social memory' than individual NPCs and so we allow
|
||||
// them to have more sentiments
|
||||
pub const FACTION_MAX_SENTIMENTS: usize = 1024;
|
||||
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.
|
||||
// NOTE: More could be added to this! For example:
|
||||
// - Animal species (dislikes spiders?)
|
||||
@ -55,39 +58,39 @@ impl Sentiments {
|
||||
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();
|
||||
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.
|
||||
///
|
||||
/// Note that sentiment get decay gets slower the harsher the sentiment is.
|
||||
/// You can calculate the **average** number of ticks required for a
|
||||
/// sentiment to decay with the following formula:
|
||||
/// You can calculate the **average** number of seconds required for a
|
||||
/// 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
|
||||
/// 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:
|
||||
///
|
||||
/// - `POSITIVE`/`NEGATIVE`: ~46 minutes
|
||||
/// - `ALLY`/`RIVAL`: ~6.9 hours
|
||||
/// - `FRIEND`/`ENEMY`: ~27.5 hours
|
||||
/// - `HERO`/`VILLAIN`: ~48.9 hours
|
||||
pub fn decay(&mut self, rng: &mut impl Rng) {
|
||||
/// - `POSITIVE`/`NEGATIVE`: ~48 minutes
|
||||
/// - `ALLY`/`RIVAL`: ~7 hours
|
||||
/// - `FRIEND`/`ENEMY`: ~29 hours
|
||||
/// - `HERO`/`VILLAIN`: ~65 hours
|
||||
pub fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
|
||||
self.map.retain(|_, sentiment| {
|
||||
sentiment.decay(rng);
|
||||
sentiment.decay(rng, dt);
|
||||
// We can eliminate redundant sentiments that don't need remembering
|
||||
!sentiment.is_redundant()
|
||||
});
|
||||
@ -152,26 +155,30 @@ impl Sentiment {
|
||||
/// generally try to harm the actor in any way they can.
|
||||
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:
|
||||
// 1) Very small changes should not be rounded to 0
|
||||
// 2) Sentiment should never (over/under)flow
|
||||
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.saturating_add(abs)
|
||||
self.positivity.saturating_add(abs).min(cap)
|
||||
} 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 {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::data::{ReportId, Reports};
|
||||
pub use common::rtsim::SiteId;
|
||||
use common::{
|
||||
rtsim::{FactionId, NpcId},
|
||||
@ -12,9 +13,14 @@ use world::site::Site as WorldSite;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Site {
|
||||
pub seed: u32,
|
||||
pub wpos: Vec2<i32>,
|
||||
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
|
||||
/// to.
|
||||
///
|
||||
@ -40,6 +46,12 @@ impl Site {
|
||||
self.faction = faction.into();
|
||||
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)]
|
||||
|
@ -3,6 +3,7 @@ use common::{
|
||||
resources::{Time, TimeOfDay},
|
||||
rtsim::Actor,
|
||||
};
|
||||
use vek::*;
|
||||
use world::{IndexRef, World};
|
||||
|
||||
pub trait Event: Clone + 'static {}
|
||||
@ -31,6 +32,7 @@ impl Event for OnTick {}
|
||||
#[derive(Clone)]
|
||||
pub struct OnDeath {
|
||||
pub actor: Actor,
|
||||
pub wpos: Option<Vec3<f32>>,
|
||||
pub killer: Option<Actor>,
|
||||
}
|
||||
impl Event for OnDeath {}
|
||||
|
@ -5,8 +5,10 @@ use world::{IndexRef, World};
|
||||
impl Faction {
|
||||
pub fn generate(_world: &World, _index: IndexRef, rng: &mut impl Rng) -> Self {
|
||||
Self {
|
||||
seed: rng.gen(),
|
||||
leader: None,
|
||||
good_or_evil: rng.gen(),
|
||||
sentiments: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ pub mod name;
|
||||
pub mod site;
|
||||
|
||||
use crate::data::{
|
||||
faction::{Faction, Factions},
|
||||
faction::Faction,
|
||||
npc::{Npc, Npcs, Profession, Vehicle},
|
||||
site::{Site, Sites},
|
||||
site::Site,
|
||||
Data, Nature,
|
||||
};
|
||||
use common::{
|
||||
@ -37,13 +37,9 @@ impl Data {
|
||||
npc_grid: Grid::new(Vec2::zero(), Default::default()),
|
||||
character_map: Default::default(),
|
||||
},
|
||||
sites: Sites {
|
||||
sites: Default::default(),
|
||||
world_site_map: Default::default(),
|
||||
},
|
||||
factions: Factions {
|
||||
factions: Default::default(),
|
||||
},
|
||||
sites: Default::default(),
|
||||
factions: Default::default(),
|
||||
reports: Default::default(),
|
||||
|
||||
tick: 0,
|
||||
time_of_day: TimeOfDay(settings.start_time),
|
||||
@ -72,6 +68,7 @@ impl Data {
|
||||
index,
|
||||
&initial_factions,
|
||||
&this.factions,
|
||||
&mut rng,
|
||||
);
|
||||
this.sites.create(site);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::data::{FactionId, Factions, Site};
|
||||
use common::store::Id;
|
||||
use hashbrown::HashSet;
|
||||
use rand::prelude::*;
|
||||
use vek::*;
|
||||
use world::{
|
||||
site::{Site as WorldSite, SiteKind},
|
||||
@ -14,6 +14,7 @@ impl Site {
|
||||
index: IndexRef,
|
||||
nearby_factions: &[(Vec2<i32>, FactionId)],
|
||||
factions: &Factions,
|
||||
rng: &mut impl Rng,
|
||||
) -> Self {
|
||||
let world_site = index.sites.get(world_site_id);
|
||||
let wpos = world_site.get_origin();
|
||||
@ -36,6 +37,7 @@ impl Site {
|
||||
};
|
||||
|
||||
Self {
|
||||
seed: rng.gen(),
|
||||
wpos,
|
||||
world_site: Some(world_site_id),
|
||||
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))
|
||||
.map(|(_, faction)| *faction)
|
||||
}),
|
||||
population: HashSet::new(),
|
||||
population: Default::default(),
|
||||
known_reports: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +62,11 @@ impl RtState {
|
||||
info!("Starting default rtsim rules...");
|
||||
self.start_rule::<rule::migrate::Migrate>();
|
||||
self.start_rule::<rule::replenish_resources::ReplenishResources>();
|
||||
self.start_rule::<rule::report::ReportEvents>();
|
||||
self.start_rule::<rule::sync_npcs::SyncNpcs>();
|
||||
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
|
||||
self.start_rule::<rule::npc_ai::NpcAi>();
|
||||
self.start_rule::<rule::cleanup::CleanUp>();
|
||||
}
|
||||
|
||||
pub fn start_rule<R: Rule>(&mut self) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
pub mod cleanup;
|
||||
pub mod migrate;
|
||||
pub mod npc_ai;
|
||||
pub mod replenish_resources;
|
||||
pub mod report;
|
||||
pub mod simulate_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 rand::prelude::*;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use tracing::warn;
|
||||
|
||||
/// 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> {
|
||||
rtstate.bind::<Self, OnSetup>(|ctx| {
|
||||
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
|
||||
data.sites.sites.retain(|site_id, site| {
|
||||
if let Some((world_site_id, _)) = ctx
|
||||
@ -55,6 +60,7 @@ impl Rule for Migrate {
|
||||
ctx.index,
|
||||
&[],
|
||||
&data.factions,
|
||||
&mut rng,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use crate::{
|
||||
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
|
||||
data::{
|
||||
npc::{Brain, PathData, SimulationMode},
|
||||
Sites,
|
||||
ReportKind, Sentiment, Sites,
|
||||
},
|
||||
event::OnTick,
|
||||
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)
|
||||
.map(|(npc_id, npc)| {
|
||||
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 {
|
||||
action: Box::new(think().repeat()),
|
||||
});
|
||||
(npc_id, controller, brain)
|
||||
(npc_id, controller, inbox, sentiments, known_reports, brain)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
@ -233,7 +236,7 @@ impl Rule for NpcAi {
|
||||
|
||||
npc_data
|
||||
.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];
|
||||
|
||||
brain.action.tick(&mut NpcCtx {
|
||||
@ -245,6 +248,9 @@ impl Rule for NpcAi {
|
||||
npc,
|
||||
npc_id: *npc_id,
|
||||
controller,
|
||||
inbox,
|
||||
known_reports,
|
||||
sentiments,
|
||||
rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
|
||||
});
|
||||
});
|
||||
@ -252,9 +258,12 @@ impl Rule for NpcAi {
|
||||
|
||||
// Reinsert NPC brains
|
||||
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].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(|_| ())
|
||||
}
|
||||
|
||||
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 {
|
||||
choose(|ctx| {
|
||||
if let Some(riding) = &ctx.npc.riding {
|
||||
@ -890,15 +943,19 @@ fn humanoid() -> impl Action {
|
||||
} else {
|
||||
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 {
|
||||
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) {
|
||||
match action {
|
||||
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())
|
||||
});
|
||||
|
||||
// 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
|
||||
let chunk_pos = npc
|
||||
.wpos
|
||||
|
@ -2069,13 +2069,20 @@ fn handle_kill_npcs(
|
||||
let to_kill = {
|
||||
let ecs = server.state.ecs();
|
||||
let entities = ecs.entities();
|
||||
let positions = ecs.write_storage::<comp::Pos>();
|
||||
let healths = ecs.write_storage::<comp::Health>();
|
||||
let players = ecs.read_storage::<comp::Player>();
|
||||
let alignments = ecs.read_storage::<Alignment>();
|
||||
|
||||
(&entities, &healths, !&players, alignments.maybe())
|
||||
(
|
||||
&entities,
|
||||
&healths,
|
||||
!&players,
|
||||
alignments.maybe(),
|
||||
&positions,
|
||||
)
|
||||
.join()
|
||||
.filter_map(|(entity, _health, (), alignment)| {
|
||||
.filter_map(|(entity, _health, (), alignment, pos)| {
|
||||
let should_kill = kill_pets
|
||||
|| if let Some(Alignment::Owned(owned)) = alignment {
|
||||
ecs.entity_from_uid(owned.0)
|
||||
@ -2095,6 +2102,7 @@ fn handle_kill_npcs(
|
||||
&ecs.read_resource::<Arc<world::World>>(),
|
||||
ecs.read_resource::<world::IndexOwned>().as_index_ref(),
|
||||
Actor::Npc(rtsim_entity.0),
|
||||
Some(pos.0),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -529,6 +529,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
.read_resource::<world::IndexOwned>()
|
||||
.as_index_ref(),
|
||||
actor,
|
||||
state.ecs().read_storage::<Pos>().get(entity).map(|p| p.0),
|
||||
last_change
|
||||
.by
|
||||
.as_ref()
|
||||
|
@ -62,13 +62,14 @@ impl RtSim {
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Rtsim data failed to load: {}", e);
|
||||
info!("Old rtsim data will now be moved to a backup file");
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let mut backup_path = file_path.clone();
|
||||
backup_path.set_extension(if i == 0 {
|
||||
format!("backup_{}", i)
|
||||
} else {
|
||||
"ron_backup".to_string()
|
||||
} else {
|
||||
format!("ron_backup_{}", i)
|
||||
});
|
||||
if !backup_path.exists() {
|
||||
fs::rename(&file_path, &backup_path)?;
|
||||
@ -78,6 +79,11 @@ impl RtSim {
|
||||
);
|
||||
info!("A fresh rtsim data will now be generated.");
|
||||
break;
|
||||
} else {
|
||||
info!(
|
||||
"Backup file {} already exists, trying another name...",
|
||||
backup_path.display()
|
||||
);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@ -169,9 +175,18 @@ impl RtSim {
|
||||
world: &World,
|
||||
index: IndexRef,
|
||||
actor: Actor,
|
||||
wpos: Option<Vec3<f32>>,
|
||||
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) {
|
||||
|
@ -501,6 +501,18 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
||||
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
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user