diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs new file mode 100644 index 0000000000..b719383d1e --- /dev/null +++ b/common/src/comp/buff.rs @@ -0,0 +1,155 @@ +use crate::sync::Uid; +use serde::{Deserialize, Serialize}; +use specs::{Component, FlaggedStorage}; +use specs_idvs::IdvStorage; +use std::time::Duration; + +/// De/buff ID. +/// ID can be independant of an actual type/config of a `BuffData`. +/// Therefore, information provided by `BuffId` can be incomplete/incorrect. +/// +/// For example, there could be two regeneration buffs, each with +/// different strength, but they could use the same `BuffId`, +/// making it harder to recognize which is which. +/// +/// Also, this should be dehardcoded eventually. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum BuffId { + /// Restores health/time for some period + Regeneration, + /// Lowers health/time for some period, but faster + Poison, + /// Changes entity name as to "Cursed {}" + Cursed, +} + +/// De/buff category ID. +/// Similar to `BuffId`, but to mark a category (for more generic usage, like +/// positive/negative buffs). +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum BuffCategoryId { + Natural, + Magical, + Divine, + Negative, + Positive, +} + +/// Data indicating and configuring behaviour of a de/buff. +/// +/// NOTE: Contents of this enum are WIP/Placeholder +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum BuffData { + /// Periodically damages health + RepeatedHealthChange { speed: f32, accumulated: f32 }, + /// Changes name on_add/on_remove + NameChange { prefix: String }, +} + +/// Actual de/buff. +/// Buff can timeout after some time if `time` is Some. If `time` is None, +/// Buff will last indefinitely, until removed manually (by some action, like +/// uncursing). The `time` field might be moved into the `Buffs` component +/// (so that `Buff` does not own this information). +/// +/// Buff has an id and data, which can be independent on each other. +/// This makes it hard to create buff stacking "helpers", as the system +/// does not assume that the same id is always the same behaviour (data). +/// Therefore id=behaviour relationship has to be enforced elsewhere (if +/// desired). +/// +/// To provide more classification info when needed, +/// buff can be in one or more buff category. +/// +/// `data` is separate, to make this system more flexible +/// (at the cost of the fact that id=behaviour relationship might not apply). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Buff { + pub id: BuffId, + pub cat_ids: Vec, + pub time: Option, + pub data: BuffData, +} + +/// Information about whether buff addition or removal was requested. +/// This to implement "on_add" and "on_remove" hooks for constant buffs. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum BuffChange { + /// Adds this buff. + Add(Buff), + /// Removes all buffs with this ID. + /// TODO: Better removal, allowing to specify which ability to remove + /// directly. + Remove(BuffId), +} + +/// Source of the de/buff +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +pub enum BuffSource { + /// Applied by a character + Character { by: Uid }, + /// Applied by world, like a poisonous fumes from a swamp + World, + /// Applied by command + Command, + /// Applied by an item + Item, + /// Applied by another buff (like an after-effect) + Buff, + /// Some other source + Unknown, +} + +/// Component holding all de/buffs that gets resolved each tick. +/// On each tick, remaining time of buffs get lowered and +/// buff effect of each buff is applied or not, depending on the `BuffData` +/// (specs system will decide based on `BuffData`, to simplify implementation). +/// TODO: Something like `once` flag for `Buff` to remove the dependence on +/// `BuffData` enum? +/// +/// In case of one-time buffs, buff effects will be applied on addition +/// and undone on removal of the buff (by the specs system). +/// Example could be decreasing max health, which, if repeated each tick, +/// would be probably an undesired effect). +/// +/// TODO: Make this net/sync-friendly. Events could help there +/// (probably replacing `changes`). Also, the "buff ticking" is really +/// not needed to be synced often, only in case that a buff begins/ends +/// (as the buff ECS system will probably run on a client too). +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Buffs { + /// Active de/buffs. + pub buffs: Vec, + /// Request to add/remove a buff. + /// Used for reacting on buff changes by the ECS system. + /// TODO: Can be `EventBus` used instead of this? + pub changes: Vec, + /// Last time since any buff change to limit syncing. + pub last_change: f64, +} + +impl Buffs { + /// Adds a request for adding given `buff`. + pub fn add_buff(&mut self, buff: Buff) { + let change = BuffChange::Add(buff); + self.changes.push(change); + self.last_change = 0.0; + } + + /// Adds a request for removal of all buffs with given Id. + /// TODO: Better removal, allowing to specify which ability to remove + /// directly. + pub fn remove_buff_by_id(&mut self, id: BuffId) { + let change = BuffChange::Remove(id); + self.changes.push(change); + self.last_change = 0.0; + } + + /// This is a primitive check if a specific buff is present. + /// (for purposes like blocking usage of abilities or something like this). + pub fn has_buff_id(&self, id: &BuffId) -> bool { self.buffs.iter().any(|buff| buff.id == *id) } +} + +impl Component for Buffs { + type Storage = FlaggedStorage>; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index c8204c6eed..ead6d4b371 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -3,6 +3,7 @@ mod admin; pub mod agent; pub mod beam; pub mod body; +mod buff; mod character_state; pub mod chat; mod controller; @@ -31,6 +32,7 @@ pub use body::{ biped_large, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, Body, BodyData, }; +pub use buff::{Buff, BuffCategoryId, BuffChange, BuffData, BuffId, Buffs}; pub use character_state::{Attacking, CharacterState, StateUpdate}; pub use chat::{ ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index ce9852720f..41ab1bda71 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -13,6 +13,7 @@ sum_type! { Player(comp::Player), CanBuild(comp::CanBuild), Stats(comp::Stats), + Buffs(comp::Buffs), Energy(comp::Energy), LightEmitter(comp::LightEmitter), Item(comp::Item), @@ -42,6 +43,7 @@ sum_type! { Player(PhantomData), CanBuild(PhantomData), Stats(PhantomData), + Buffs(PhantomData), Energy(PhantomData), LightEmitter(PhantomData), Item(PhantomData), @@ -71,6 +73,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Player(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), @@ -98,6 +101,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Player(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), @@ -125,6 +129,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Player(_) => sync::handle_remove::(entity, world), EcsCompPhantom::CanBuild(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Stats(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Buffs(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Energy(_) => sync::handle_remove::(entity, world), EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::(entity, world) diff --git a/common/src/state.rs b/common/src/state.rs index d54a5d9433..cbf00daaf2 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -112,6 +112,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/sys/buff.rs b/common/src/sys/buff.rs new file mode 100644 index 0000000000..32b5150f46 --- /dev/null +++ b/common/src/sys/buff.rs @@ -0,0 +1,130 @@ +use crate::{ + comp::{BuffChange, BuffData, BuffId, Buffs, HealthChange, HealthSource, Stats}, + state::DeltaTime, +}; +use specs::{Entities, Join, Read, System, WriteStorage}; +use std::time::Duration; + +/// This system modifies entity stats, changing them using buffs +/// Currently, the system is VERY, VERY CRUDE and SYNC UN-FRIENDLY. +/// It does not use events and uses `Vec`s stored in component. +/// +/// TODO: Make this production-quality system/design +pub struct Sys; +impl<'a> System<'a> for Sys { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + Read<'a, DeltaTime>, + WriteStorage<'a, Stats>, + WriteStorage<'a, Buffs>, + ); + + fn run(&mut self, (entities, dt, mut stats, mut buffs): Self::SystemData) { + // Increment last change timer + buffs.set_event_emission(false); + for buff in (&mut buffs).join() { + buff.last_change += f64::from(dt.0); + } + buffs.set_event_emission(true); + + for (entity, mut buffs) in (&entities, &mut buffs.restrict_mut()).join() { + let buff_comp = buffs.get_mut_unchecked(); + // Add/Remove de/buffs + // While adding/removing buffs, it could call respective hooks + // Currently, it is done based on enum variant + let changes = buff_comp.changes.drain(0..buff_comp.changes.len()); + for change in changes { + match change { + // Hooks for on_add could be here + BuffChange::Add(new_buff) => { + match &new_buff.data { + BuffData::NameChange { prefix } => { + if let Some(stats) = stats.get_mut(entity) { + let mut pref = String::from(prefix); + pref.push_str(&stats.name); + stats.name = pref; + } + }, + _ => {}, + } + buff_comp.buffs.push(new_buff.clone()); + }, + // Hooks for on_remove could be here + BuffChange::Remove(id) => { + let some_predicate = |current_id: &BuffId| *current_id == id; + let mut i = 0; + while i != buff_comp.buffs.len() { + if some_predicate(&mut buff_comp.buffs[i].id) { + let buff = buff_comp.buffs.remove(i); + match &buff.data { + BuffData::NameChange { prefix } => { + if let Some(stats) = stats.get_mut(entity) { + stats.name = stats.name.replace(prefix, ""); + } + }, + _ => {}, + } + } else { + i += 1; + } + } + }, + } + } + + let mut buffs_for_removal = Vec::new(); + // Tick all de/buffs on a Buffs component. + for active_buff in &mut buff_comp.buffs { + // First, tick the buff and subtract delta from it + // and return how much "real" time the buff took (for tick independence). + // TODO: handle delta for "indefinite" buffs, i.e. time since they got removed. + let buff_delta = if let Some(remaining_time) = &mut active_buff.time { + let pre_tick = remaining_time.as_secs_f32(); + let new_duration = remaining_time.checked_sub(Duration::from_secs_f32(dt.0)); + let post_tick = if let Some(dur) = new_duration { + // The buff still continues. + *remaining_time -= Duration::from_secs_f32(dt.0); + dur.as_secs_f32() + } else { + // The buff has expired. + // Mark it for removal. + // TODO: This removes by ID! better method required + buffs_for_removal.push(active_buff.id.clone()); + 0.0 + }; + pre_tick - post_tick + } else { + // The buff is indefinite, and it takes full tick (delta). + // TODO: Delta for indefinite buffs might be shorter since they can get removed + // *during a tick* and this treats it as it always happens on a *tick end*. + dt.0 + }; + + // Now, execute the buff, based on it's delta + match &mut active_buff.data { + BuffData::RepeatedHealthChange { speed, accumulated } => { + *accumulated += *speed * buff_delta; + // Apply only 0.5 or higher damage + if accumulated.abs() > 5.0 { + if let Some(stats) = stats.get_mut(entity) { + let change = HealthChange { + amount: *accumulated as i32, + cause: HealthSource::Unknown, + }; + stats.health.change_by(change); + } + *accumulated = 0.0; + }; + }, + _ => {}, + }; + } + // Truly mark expired buffs for removal. + // TODO: Review this, as it is ugly. + for to_remove in buffs_for_removal { + buff_comp.remove_buff_by_id(to_remove); + } + } + } +} diff --git a/common/src/sys/mod.rs b/common/src/sys/mod.rs index 99b2e56047..98c70fd145 100644 --- a/common/src/sys/mod.rs +++ b/common/src/sys/mod.rs @@ -1,5 +1,6 @@ pub mod agent; mod beam; +mod buff; pub mod character_behavior; pub mod combat; pub mod controller; @@ -23,6 +24,7 @@ pub const PHYS_SYS: &str = "phys_sys"; pub const PROJECTILE_SYS: &str = "projectile_sys"; pub const SHOCKWAVE_SYS: &str = "shockwave_sys"; pub const STATS_SYS: &str = "stats_sys"; +pub const BUFFS_SYS: &str = "buffs_sys"; pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(agent::Sys, AGENT_SYS, &[]); @@ -32,6 +34,7 @@ pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { CONTROLLER_SYS, ]); dispatch_builder.add(stats::Sys, STATS_SYS, &[]); + dispatch_builder.add(buff::Sys, BUFFS_SYS, &[]); dispatch_builder.add(phys::Sys, PHYS_SYS, &[CONTROLLER_SYS, MOUNT_SYS, STATS_SYS]); dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]); dispatch_builder.add(shockwave::Sys, SHOCKWAVE_SYS, &[PHYS_SYS]); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a4208f0934..0fc1b657d5 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -93,6 +93,39 @@ impl StateExt for State { loadout: comp::Loadout, body: comp::Body, ) -> EcsEntityBuilder { + // NOTE: This is placeholder code for testing and does not + // belongs here really + let mut buffs = comp::Buffs::default(); + // Damages slightly each NPC spawned for 20 seconds + buffs.add_buff(comp::Buff { + id: comp::BuffId::Poison, + cat_ids: vec![], + time: Some(std::time::Duration::from_secs(20)), + data: comp::BuffData::RepeatedHealthChange { + speed: -10.0, + accumulated: 0.0, + }, + }); + // Prepends "Cursed " to each NPC's name for 35 secs + buffs.add_buff(comp::Buff { + id: comp::BuffId::Cursed, + cat_ids: vec![], + time: Some(std::time::Duration::from_secs(35)), + data: comp::BuffData::NameChange { + prefix: String::from("Cursed "), + }, + }); + // Adds super-slow regen to each NPC spawned, indefinitely + buffs.add_buff(comp::Buff { + id: comp::BuffId::Regeneration, + cat_ids: vec![], + time: None, + data: comp::BuffData::RepeatedHealthChange { + speed: 1.0, + accumulated: 0.0, + }, + }); + self.ecs_mut() .create_entity_synced() .with(pos) @@ -111,6 +144,7 @@ impl StateExt for State { .with(comp::Gravity(1.0)) .with(comp::CharacterState::default()) .with(loadout) + .with(buffs) } fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index fcecccb288..68d568b66c 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,7 +1,7 @@ use super::SysTimer; use common::{ comp::{ - BeamSegment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, + BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, Stats, Sticky, Vel, }, @@ -44,6 +44,7 @@ pub struct TrackedComps<'a> { pub body: ReadStorage<'a, Body>, pub player: ReadStorage<'a, Player>, pub stats: ReadStorage<'a, Stats>, + pub buffs: ReadStorage<'a, Buffs>, pub energy: ReadStorage<'a, Energy>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, @@ -85,6 +86,10 @@ impl<'a> TrackedComps<'a> { .get(entity) .cloned() .map(|c| comps.push(c.into())); + self.buffs + .get(entity) + .cloned() + .map(|c| comps.push(c.into())); self.energy .get(entity) .cloned() @@ -157,6 +162,7 @@ pub struct ReadTrackers<'a> { pub body: ReadExpect<'a, UpdateTracker>, pub player: ReadExpect<'a, UpdateTracker>, pub stats: ReadExpect<'a, UpdateTracker>, + pub buffs: ReadExpect<'a, UpdateTracker>, pub energy: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, @@ -187,6 +193,7 @@ impl<'a> ReadTrackers<'a> { .with_component(&comps.uid, &*self.body, &comps.body, filter) .with_component(&comps.uid, &*self.player, &comps.player, filter) .with_component(&comps.uid, &*self.stats, &comps.stats, filter) + .with_component(&comps.uid, &*self.buffs, &comps.buffs, filter) .with_component(&comps.uid, &*self.energy, &comps.energy, filter) .with_component(&comps.uid, &*self.can_build, &comps.can_build, filter) .with_component( @@ -224,6 +231,7 @@ pub struct WriteTrackers<'a> { body: WriteExpect<'a, UpdateTracker>, player: WriteExpect<'a, UpdateTracker>, stats: WriteExpect<'a, UpdateTracker>, + buffs: WriteExpect<'a, UpdateTracker>, energy: WriteExpect<'a, UpdateTracker>, can_build: WriteExpect<'a, UpdateTracker>, light_emitter: WriteExpect<'a, UpdateTracker>, @@ -248,6 +256,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.body.record_changes(&comps.body); trackers.player.record_changes(&comps.player); trackers.stats.record_changes(&comps.stats); + trackers.buffs.record_changes(&comps.buffs); trackers.energy.record_changes(&comps.energy); trackers.can_build.record_changes(&comps.can_build); trackers.light_emitter.record_changes(&comps.light_emitter); @@ -283,6 +292,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { }; log_counts!(uid, "Uids"); log_counts!(body, "Bodies"); + log_counts!(buffs, "Buffs"); log_counts!(player, "Players"); log_counts!(stats, "Stats"); log_counts!(energy, "Energies"); @@ -307,6 +317,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::();