Initial WIP implementation of the Buff system

This commit is contained in:
BottledByte 2020-08-11 00:54:45 +02:00 committed by Sam
parent 32d707e57f
commit 7ab99a3bbf
8 changed files with 342 additions and 1 deletions

155
common/src/comp/buff.rs Normal file
View File

@ -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<BuffCategoryId>,
pub time: Option<Duration>,
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<Buff>,
/// Request to add/remove a buff.
/// Used for reacting on buff changes by the ECS system.
/// TODO: Can be `EventBus<T>` used instead of this?
pub changes: Vec<BuffChange>,
/// 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<Self, IdvStorage<Self>>;
}

View File

@ -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,

View File

@ -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<comp::Player>),
CanBuild(PhantomData<comp::CanBuild>),
Stats(PhantomData<comp::Stats>),
Buffs(PhantomData<comp::Buffs>),
Energy(PhantomData<comp::Energy>),
LightEmitter(PhantomData<comp::LightEmitter>),
Item(PhantomData<comp::Item>),
@ -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::<comp::Player>(entity, world),
EcsCompPhantom::CanBuild(_) => sync::handle_remove::<comp::CanBuild>(entity, world),
EcsCompPhantom::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world),
EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world),
EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)

View File

@ -112,6 +112,7 @@ impl State {
ecs.register::<comp::Body>();
ecs.register::<comp::Player>();
ecs.register::<comp::Stats>();
ecs.register::<comp::Buffs>();
ecs.register::<comp::Energy>();
ecs.register::<comp::CanBuild>();
ecs.register::<comp::LightEmitter>();

130
common/src/sys/buff.rs Normal file
View File

@ -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);
}
}
}
}

View File

@ -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]);

View File

@ -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 {

View File

@ -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<Body>>,
pub player: ReadExpect<'a, UpdateTracker<Player>>,
pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
@ -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<Body>>,
player: WriteExpect<'a, UpdateTracker<Player>>,
stats: WriteExpect<'a, UpdateTracker<Stats>>,
buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
@ -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::<Body>();
world.register_tracker::<Player>();
world.register_tracker::<Stats>();
world.register_tracker::<Buffs>();
world.register_tracker::<Energy>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();