Merge branch 'sam/buffs' into 'master'

Auras and buffs no longer need to tick every tick

See merge request veloren/veloren!3814
This commit is contained in:
Samuel Keiffer
2023-03-12 22:06:48 +00:00
69 changed files with 831 additions and 751 deletions

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Potion, kind: Potion,
data: ( data: (
strength: 100.0, strength: 100.0,
duration: Some(( duration: Some(1),
secs: 1,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,8 +16,8 @@ ItemDef(
kind: PotionSickness, kind: PotionSickness,
data: ( data: (
strength: 0.33, strength: 0.33,
duration: Some(( secs: 45, nanos: 0, )), duration: Some(45),
delay: Some(( secs: 1, nanos: 0, )) delay: Some(1)
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Potion, kind: Potion,
data: ( data: (
strength: 100.0, strength: 100.0,
duration: Some(( duration: Some(1),
secs: 1,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,8 +16,7 @@ ItemDef(
kind: PotionSickness, kind: PotionSickness,
data: ( data: (
strength: 0.33, strength: 0.33,
duration: Some(( secs: 45, nanos: 0, )), duration: Some(45),
delay: Some(( secs: 1, nanos: 0, ))
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Potion, kind: Potion,
data: ( data: (
strength: 75.0, strength: 75.0,
duration: Some(( duration: Some(1),
secs: 1,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,8 +16,7 @@ ItemDef(
kind: PotionSickness, kind: PotionSickness,
data: ( data: (
strength: 0.33, strength: 0.33,
duration: Some(( secs: 45, nanos: 0, )), duration: Some(45),
delay: Some(( secs: 1, nanos: 0, ))
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Potion, kind: Potion,
data: ( data: (
strength: 50.0, strength: 50.0,
duration: Some(( duration: Some(1),
secs: 1,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,8 +16,7 @@ ItemDef(
kind: PotionSickness, kind: PotionSickness,
data: ( data: (
strength: 0.33, strength: 0.33,
duration: Some(( secs: 45, nanos: 0, )), duration: Some(45),
delay: Some(( secs: 1, nanos: 0, ))
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Regeneration, kind: Regeneration,
data: ( data: (
strength: 1000, strength: 1000,
duration: Some(( duration: Some(999),
secs: 999,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,10 +16,7 @@ ItemDef(
kind: EnergyRegen, kind: EnergyRegen,
data: ( data: (
strength: 1000, strength: 1000,
duration: Some(( duration: Some(999),
secs: 999,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -30,10 +24,7 @@ ItemDef(
kind: IncreaseMaxHealth, kind: IncreaseMaxHealth,
data: ( data: (
strength: 50000, strength: 50000,
duration: Some(( duration: Some(999),
secs: 999,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -41,10 +32,7 @@ ItemDef(
kind: IncreaseMaxEnergy, kind: IncreaseMaxEnergy,
data: ( data: (
strength: 50000, strength: 50000,
duration: Some(( duration: Some(999),
secs: 999,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 10.0, strength: 10.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,10 +16,7 @@ ItemDef(
kind: Regeneration, kind: Regeneration,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(70),
secs: 70,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 5.0, strength: 5.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 3.0, strength: 3.0,
duration: Some(( duration: Some(20),
secs: 20,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.5, strength: 1.5,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 4.0, strength: 4.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 4.0, strength: 4.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.5, strength: 2.5,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .45, strength: .45,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.5, strength: 1.5,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .25, strength: .25,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.5, strength: 2.5,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 5.5, strength: 5.5,
duration: Some(( duration: Some(15),
secs: 15,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .9, strength: .9,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .45, strength: .45,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.5, strength: 2.5,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .45, strength: .45,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: .36, strength: .36,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 4.0, strength: 4.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Frenzied, kind: Frenzied,
data: ( data: (
strength: 0.4, strength: 0.4,
duration: Some(( duration: Some(60),
secs: 60,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),
@ -19,10 +16,7 @@ ItemDef(
kind: Cursed, kind: Cursed,
data: ( data: (
strength: 0.35, strength: 0.35,
duration: Some(( duration: Some(60),
secs: 60,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 2.0, strength: 2.0,
duration: Some(( duration: Some(10),
secs: 10,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 1.0, strength: 1.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -8,10 +8,7 @@ ItemDef(
kind: Saturation, kind: Saturation,
data: ( data: (
strength: 3.0, strength: 3.0,
duration: Some(( duration: Some(5),
secs: 5,
nanos: 0,
)),
), ),
cat_ids: [Natural], cat_ids: [Natural],
)), )),

View File

@ -2135,9 +2135,10 @@ impl Client {
return Err(Error::Other("Failed to find entity from uid.".into())); return Err(Error::Other("Failed to find entity from uid.".into()));
} }
}, },
ServerGeneral::TimeOfDay(time_of_day, calendar) => { ServerGeneral::TimeOfDay(time_of_day, calendar, time) => {
self.target_time_of_day = Some(time_of_day); self.target_time_of_day = Some(time_of_day);
*self.state.ecs_mut().write_resource() = calendar; *self.state.ecs_mut().write_resource() = calendar;
*self.state.ecs_mut().write_resource() = time;
}, },
ServerGeneral::EntitySync(entity_sync_package) => { ServerGeneral::EntitySync(entity_sync_package) => {
self.state self.state

View File

@ -11,7 +11,7 @@ use common::{
lod, lod,
outcome::Outcome, outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook}, recipe::{ComponentRecipeBook, RecipeBook},
resources::TimeOfDay, resources::{Time, TimeOfDay},
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult}, trade::{PendingTrade, SitePrices, TradeId, TradeResult},
uid::Uid, uid::Uid,
@ -192,7 +192,7 @@ pub enum ServerGeneral {
ChatMsg(comp::ChatMsg), ChatMsg(comp::ChatMsg),
ChatMode(comp::ChatMode), ChatMode(comp::ChatMode),
SetPlayerEntity(Uid), SetPlayerEntity(Uid),
TimeOfDay(TimeOfDay, Calendar), TimeOfDay(TimeOfDay, Calendar, Time),
EntitySync(sync::EntitySyncPackage), EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>, u64), CompSync(sync::CompSyncPackage<EcsCompPacket>, u64),
CreateEntity(sync::EntityPackage<EcsCompPacket>), CreateEntity(sync::EntityPackage<EcsCompPacket>),
@ -340,7 +340,7 @@ impl ServerMsg {
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_) | ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _) | ServerGeneral::TimeOfDay(_, _, _)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _) | ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_) | ServerGeneral::CreateEntity(_)

View File

@ -13,6 +13,7 @@ use crate::{
}, },
event::ServerEvent, event::ServerEvent,
outcome::Outcome, outcome::Outcome,
resources::Secs,
states::utils::StageSection, states::utils::StageSection,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
util::Dir, util::Dir,
@ -27,7 +28,7 @@ use crate::{comp::Group, resources::Time};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage}; use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use std::{ops::MulAssign, time::Duration}; use std::ops::MulAssign;
#[cfg(not(target_arch = "wasm32"))] use vek::*; #[cfg(not(target_arch = "wasm32"))] use vek::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -363,7 +364,9 @@ impl Attack {
emit(ServerEvent::Buff { emit(ServerEvent::Buff {
entity: target.entity, entity: target.entity,
buff_change: BuffChange::Add(b.to_buff( buff_change: BuffChange::Add(b.to_buff(
time,
attacker.map(|a| a.uid), attacker.map(|a| a.uid),
target.stats,
applied_damage, applied_damage,
strength_modifier, strength_modifier,
)), )),
@ -529,7 +532,9 @@ impl Attack {
emit(ServerEvent::Buff { emit(ServerEvent::Buff {
entity: target.entity, entity: target.entity,
buff_change: BuffChange::Add(b.to_buff( buff_change: BuffChange::Add(b.to_buff(
time,
attacker.map(|a| a.uid), attacker.map(|a| a.uid),
target.stats,
accumulated_damage, accumulated_damage,
strength_modifier, strength_modifier,
)), )),
@ -1027,7 +1032,14 @@ impl MulAssign<f32> for CombatBuffStrength {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
impl CombatBuff { impl CombatBuff {
fn to_buff(self, uid: Option<Uid>, damage: f32, strength_modifier: f32) -> Buff { fn to_buff(
self,
time: Time,
uid: Option<Uid>,
stats: Option<&Stats>,
damage: f32,
strength_modifier: f32,
) -> Buff {
// TODO: Generate BufCategoryId vec (probably requires damage overhaul?) // TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
let source = if let Some(uid) = uid { let source = if let Some(uid) = uid {
BuffSource::Character { by: uid } BuffSource::Character { by: uid }
@ -1038,11 +1050,13 @@ impl CombatBuff {
self.kind, self.kind,
BuffData::new( BuffData::new(
self.strength.to_strength(damage, strength_modifier), self.strength.to_strength(damage, strength_modifier),
Some(Duration::from_secs_f32(self.dur_secs)), Some(Secs(self.dur_secs as f64)),
None, None,
), ),
Vec::new(), Vec::new(),
source, source,
time,
stats,
) )
} }
} }

View File

@ -20,6 +20,7 @@ use crate::{
}, },
Body, CharacterState, LightEmitter, StateUpdate, Body, CharacterState, LightEmitter, StateUpdate,
}, },
resources::Secs,
states::{ states::{
behavior::JoinData, behavior::JoinData,
utils::{AbilityInfo, StageSection}, utils::{AbilityInfo, StageSection},
@ -696,7 +697,7 @@ pub enum CharacterAbility {
recover_duration: f32, recover_duration: f32,
targets: combat::GroupTarget, targets: combat::GroupTarget,
auras: Vec<aura::AuraBuffConstructor>, auras: Vec<aura::AuraBuffConstructor>,
aura_duration: f32, aura_duration: Secs,
range: f32, range: f32,
energy_cost: f32, energy_cost: f32,
scales_with_combo: bool, scales_with_combo: bool,
@ -728,7 +729,7 @@ pub enum CharacterAbility {
recover_duration: f32, recover_duration: f32,
buff_kind: buff::BuffKind, buff_kind: buff::BuffKind,
buff_strength: f32, buff_strength: f32,
buff_duration: Option<f32>, buff_duration: Option<Secs>,
energy_cost: f32, energy_cost: f32,
#[serde(default)] #[serde(default)]
meta: AbilityMeta, meta: AbilityMeta,
@ -2051,7 +2052,7 @@ impl CharacterAbility {
if let Ok(level) = skillset.skill_level(Sceptre(HDuration)) { if let Ok(level) = skillset.skill_level(Sceptre(HDuration)) {
auras.iter_mut().for_each(|ref mut aura| { auras.iter_mut().for_each(|ref mut aura| {
if let Some(ref mut duration) = aura.duration { if let Some(ref mut duration) = aura.duration {
*duration *= modifiers.duration.powi(level.into()); *duration *= modifiers.duration.powi(level.into()) as f64;
} }
}); });
} }
@ -2078,7 +2079,7 @@ impl CharacterAbility {
if let Ok(level) = skillset.skill_level(Sceptre(ADuration)) { if let Ok(level) = skillset.skill_level(Sceptre(ADuration)) {
auras.iter_mut().for_each(|ref mut aura| { auras.iter_mut().for_each(|ref mut aura| {
if let Some(ref mut duration) = aura.duration { if let Some(ref mut duration) = aura.duration {
*duration *= modifiers.duration.powi(level.into()); *duration *= modifiers.duration.powi(level.into()) as f64;
} }
}); });
} }
@ -2602,7 +2603,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
recover_duration: Duration::from_secs_f32(*recover_duration), recover_duration: Duration::from_secs_f32(*recover_duration),
targets: *targets, targets: *targets,
auras: auras.clone(), auras: auras.clone(),
aura_duration: Duration::from_secs_f32(*aura_duration), aura_duration: *aura_duration,
range: *range, range: *range,
ability_info, ability_info,
scales_with_combo: *scales_with_combo, scales_with_combo: *scales_with_combo,
@ -2667,7 +2668,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
recover_duration: Duration::from_secs_f32(*recover_duration), recover_duration: Duration::from_secs_f32(*recover_duration),
buff_kind: *buff_kind, buff_kind: *buff_kind,
buff_strength: *buff_strength, buff_strength: *buff_strength,
buff_duration: buff_duration.map(Duration::from_secs_f32), buff_duration: *buff_duration,
ability_info, ability_info,
}, },
timer: Duration::default(), timer: Duration::default(),

View File

@ -1,12 +1,12 @@
use crate::{ use crate::{
combat::GroupTarget, combat::GroupTarget,
comp::buff::{BuffCategory, BuffData, BuffKind, BuffSource}, comp::buff::{BuffCategory, BuffData, BuffKind, BuffSource},
resources::{Secs, Time},
uid::Uid, uid::Uid,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slotmap::{new_key_type, SlotMap}; use slotmap::{new_key_type, SlotMap};
use specs::{Component, DerefFlaggedStorage, VecStorage}; use specs::{Component, DerefFlaggedStorage, VecStorage};
use std::time::Duration;
new_key_type! { pub struct AuraKey; } new_key_type! { pub struct AuraKey; }
@ -36,8 +36,8 @@ pub struct Aura {
pub aura_kind: AuraKind, pub aura_kind: AuraKind,
/// The radius of the aura /// The radius of the aura
pub radius: f32, pub radius: f32,
/// How long the aura lasts. None corresponds to an indefinite length // None corresponds to an indefinite aura
pub duration: Option<Duration>, pub end_time: Option<Time>,
/* TODO: Add functionality for fading or a gradient */ /* TODO: Add functionality for fading or a gradient */
/// Used to filter which entities this aura will apply to. For example, /// Used to filter which entities this aura will apply to. For example,
/// globally neutral auras which affect all entities will have the type /// globally neutral auras which affect all entities will have the type
@ -92,12 +92,12 @@ impl From<(Option<GroupTarget>, Option<&Uid>)> for AuraTarget {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuraData { pub struct AuraData {
pub duration: Option<Duration>, pub duration: Option<Secs>,
} }
impl AuraData { impl AuraData {
#[must_use] #[must_use]
fn new(duration: Option<Duration>) -> Self { Self { duration } } fn new(duration: Option<Secs>) -> Self { Self { duration } }
} }
impl Aura { impl Aura {
@ -105,13 +105,14 @@ impl Aura {
pub fn new( pub fn new(
aura_kind: AuraKind, aura_kind: AuraKind,
radius: f32, radius: f32,
duration: Option<Duration>, duration: Option<Secs>,
target: AuraTarget, target: AuraTarget,
time: Time,
) -> Self { ) -> Self {
Self { Self {
aura_kind, aura_kind,
radius, radius,
duration, end_time: duration.map(|dur| Time(time.0 + dur.0)),
target, target,
data: AuraData::new(duration), data: AuraData::new(duration),
} }
@ -142,7 +143,7 @@ impl Auras {
pub struct AuraBuffConstructor { pub struct AuraBuffConstructor {
pub kind: BuffKind, pub kind: BuffKind,
pub strength: f32, pub strength: f32,
pub duration: Option<f32>, pub duration: Option<Secs>,
pub category: BuffCategory, pub category: BuffCategory,
} }
@ -151,20 +152,21 @@ impl AuraBuffConstructor {
self, self,
uid: &Uid, uid: &Uid,
radius: f32, radius: f32,
duration: Option<Duration>, duration: Option<Secs>,
target: AuraTarget, target: AuraTarget,
time: Time,
) -> Aura { ) -> Aura {
let aura_kind = AuraKind::Buff { let aura_kind = AuraKind::Buff {
kind: self.kind, kind: self.kind,
data: BuffData { data: BuffData {
strength: self.strength, strength: self.strength,
duration: self.duration.map(Duration::from_secs_f32), duration: self.duration,
delay: None, delay: None,
}, },
category: self.category, category: self.category,
source: BuffSource::Character { by: *uid }, source: BuffSource::Character { by: *uid },
}; };
Aura::new(aura_kind, radius, duration, target) Aura::new(aura_kind, radius, duration, target, time)
} }
} }

View File

@ -1,6 +1,10 @@
#![allow(clippy::nonstandard_macro_braces)] //tmp as of false positive !? #![allow(clippy::nonstandard_macro_braces)] //tmp as of false positive !?
use crate::uid::Uid; use crate::{
use core::{cmp::Ordering, time::Duration}; comp::{aura::AuraKey, Stats},
resources::{Secs, Time},
uid::Uid,
};
use core::cmp::Ordering;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use hashbrown::HashMap; use hashbrown::HashMap;
use itertools::Either; use itertools::Either;
@ -143,13 +147,13 @@ impl BuffKind {
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct BuffData { pub struct BuffData {
pub strength: f32, pub strength: f32,
pub duration: Option<Duration>, pub duration: Option<Secs>,
pub delay: Option<Duration>, pub delay: Option<Secs>,
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
impl BuffData { impl BuffData {
pub fn new(strength: f32, duration: Option<Duration>, delay: Option<Duration>) -> Self { pub fn new(strength: f32, duration: Option<Secs>, delay: Option<Secs>) -> Self {
Self { Self {
strength, strength,
duration, duration,
@ -168,7 +172,7 @@ pub enum BuffCategory {
Magical, Magical,
Divine, Divine,
PersistOnDeath, PersistOnDeath,
FromAura(bool), // bool used to check if buff recently set by aura FromActiveAura(Uid, AuraKey),
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -183,16 +187,11 @@ pub enum BuffEffect {
/// Periodically damages or heals entity /// Periodically damages or heals entity
HealthChangeOverTime { HealthChangeOverTime {
rate: f32, rate: f32,
accumulated: f32,
kind: ModifierKind, kind: ModifierKind,
instance: u64, instance: u64,
}, },
/// Periodically consume entity energy /// Periodically consume entity energy
EnergyChangeOverTime { EnergyChangeOverTime { rate: f32, kind: ModifierKind },
rate: f32,
accumulated: f32,
kind: ModifierKind,
},
/// Changes maximum health by a certain amount /// Changes maximum health by a certain amount
MaxHealthModifier { value: f32, kind: ModifierKind }, MaxHealthModifier { value: f32, kind: ModifierKind },
/// Changes maximum energy by a certain amount /// Changes maximum energy by a certain amount
@ -204,7 +203,6 @@ pub enum BuffEffect {
rate: f32, rate: f32,
kind: ModifierKind, kind: ModifierKind,
target_fraction: f32, target_fraction: f32,
achieved_fraction: Option<f32>,
}, },
/// Modifies move speed of target /// Modifies move speed of target
MovementSpeed(f32), MovementSpeed(f32),
@ -215,7 +213,7 @@ pub enum BuffEffect {
/// Reduces poise damage taken after armor is accounted for by this fraction /// Reduces poise damage taken after armor is accounted for by this fraction
PoiseReduction(f32), PoiseReduction(f32),
/// Reduces amount healed by consumables /// Reduces amount healed by consumables
HealReduction { rate: f32 }, HealReduction(f32),
} }
/// Actual de/buff. /// Actual de/buff.
@ -233,8 +231,8 @@ pub struct Buff {
pub kind: BuffKind, pub kind: BuffKind,
pub data: BuffData, pub data: BuffData,
pub cat_ids: Vec<BuffCategory>, pub cat_ids: Vec<BuffCategory>,
pub time: Option<Duration>, pub end_time: Option<Time>,
pub delay: Option<Duration>, pub start_time: Time,
pub effects: Vec<BuffEffect>, pub effects: Vec<BuffEffect>,
pub source: BuffSource, pub source: BuffSource,
} }
@ -270,179 +268,129 @@ impl Buff {
data: BuffData, data: BuffData,
cat_ids: Vec<BuffCategory>, cat_ids: Vec<BuffCategory>,
source: BuffSource, source: BuffSource,
time: Time,
stats: Option<&Stats>,
) -> Self { ) -> Self {
// Normalized nonlinear scaling // Normalized nonlinear scaling
let nn_scaling = |a| a / (a + 0.5); let nn_scaling = |a| a / (a + 0.5);
let instance = rand::random(); let instance = rand::random();
let (effects, time) = match kind { let effects = match kind {
BuffKind::Bleeding => ( BuffKind::Bleeding => vec![BuffEffect::HealthChangeOverTime {
vec![BuffEffect::HealthChangeOverTime {
rate: -data.strength, rate: -data.strength,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}], }],
data.duration, BuffKind::Regeneration | BuffKind::Saturation => {
),
BuffKind::Regeneration | BuffKind::Saturation | BuffKind::Potion => (
vec![BuffEffect::HealthChangeOverTime { vec![BuffEffect::HealthChangeOverTime {
rate: data.strength, rate: data.strength,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}], }]
data.duration, },
), BuffKind::Potion => {
BuffKind::CampfireHeal => (
vec![BuffEffect::HealthChangeOverTime { vec![BuffEffect::HealthChangeOverTime {
rate: data.strength * stats.map_or(1.0, |s| s.heal_multiplier),
kind: ModifierKind::Additive,
instance,
}]
},
BuffKind::CampfireHeal => vec![BuffEffect::HealthChangeOverTime {
rate: data.strength, rate: data.strength,
accumulated: 0.0,
kind: ModifierKind::Fractional, kind: ModifierKind::Fractional,
instance, instance,
}], }],
data.duration, BuffKind::Cursed => vec![
),
BuffKind::Cursed => (
vec![
BuffEffect::MaxHealthChangeOverTime { BuffEffect::MaxHealthChangeOverTime {
rate: -1.0, rate: -1.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
target_fraction: 1.0 - data.strength, target_fraction: 1.0 - data.strength,
achieved_fraction: None,
}, },
BuffEffect::HealthChangeOverTime { BuffEffect::HealthChangeOverTime {
rate: -1.0, rate: -1.0,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}, },
], ],
data.duration, BuffKind::EnergyRegen => vec![BuffEffect::EnergyChangeOverTime {
),
BuffKind::EnergyRegen => (
vec![BuffEffect::EnergyChangeOverTime {
rate: data.strength, rate: data.strength,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
}], }],
data.duration, BuffKind::IncreaseMaxEnergy => vec![BuffEffect::MaxEnergyModifier {
),
BuffKind::IncreaseMaxEnergy => (
vec![BuffEffect::MaxEnergyModifier {
value: data.strength, value: data.strength,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
}], }],
data.duration, BuffKind::IncreaseMaxHealth => vec![BuffEffect::MaxHealthModifier {
),
BuffKind::IncreaseMaxHealth => (
vec![BuffEffect::MaxHealthModifier {
value: data.strength, value: data.strength,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
}], }],
data.duration, BuffKind::Invulnerability => vec![BuffEffect::DamageReduction(1.0)],
), BuffKind::ProtectingWard => vec![BuffEffect::DamageReduction(
BuffKind::Invulnerability => (vec![BuffEffect::DamageReduction(1.0)], data.duration),
BuffKind::ProtectingWard => (
vec![BuffEffect::DamageReduction(
// Causes non-linearity in effect strength, but necessary // Causes non-linearity in effect strength, but necessary
// to allow for tool power and other things to affect the // to allow for tool power and other things to affect the
// strength. 0.5 also still provides 50% damage reduction. // strength. 0.5 also still provides 50% damage reduction.
nn_scaling(data.strength), nn_scaling(data.strength),
)], )],
data.duration, BuffKind::Burning => vec![BuffEffect::HealthChangeOverTime {
),
BuffKind::Burning => (
vec![BuffEffect::HealthChangeOverTime {
rate: -data.strength, rate: -data.strength,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}], }],
data.duration, BuffKind::Poisoned => vec![BuffEffect::EnergyChangeOverTime {
),
BuffKind::Poisoned => (
vec![BuffEffect::EnergyChangeOverTime {
rate: -data.strength, rate: -data.strength,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
}], }],
data.duration, BuffKind::Crippled => vec![
),
BuffKind::Crippled => (
vec![
BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength)), BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength)),
BuffEffect::HealthChangeOverTime { BuffEffect::HealthChangeOverTime {
rate: -data.strength * 4.0, rate: -data.strength * 4.0,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}, },
], ],
data.duration, BuffKind::Frenzied => vec![
),
BuffKind::Frenzied => (
vec![
BuffEffect::MovementSpeed(1.0 + data.strength), BuffEffect::MovementSpeed(1.0 + data.strength),
BuffEffect::HealthChangeOverTime { BuffEffect::HealthChangeOverTime {
rate: data.strength * 10.0, rate: data.strength * 10.0,
accumulated: 0.0,
kind: ModifierKind::Additive, kind: ModifierKind::Additive,
instance, instance,
}, },
], ],
data.duration, BuffKind::Frozen => vec![
),
BuffKind::Frozen => (
vec![
BuffEffect::MovementSpeed(f32::powf(1.0 - nn_scaling(data.strength), 1.1)), BuffEffect::MovementSpeed(f32::powf(1.0 - nn_scaling(data.strength), 1.1)),
BuffEffect::AttackSpeed(1.0 - nn_scaling(data.strength)), BuffEffect::AttackSpeed(1.0 - nn_scaling(data.strength)),
], ],
data.duration, BuffKind::Wet => vec![BuffEffect::GroundFriction(1.0 - nn_scaling(data.strength))],
), BuffKind::Ensnared => vec![BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength))],
BuffKind::Wet => ( BuffKind::Hastened => vec![
vec![BuffEffect::GroundFriction(1.0 - nn_scaling(data.strength))],
data.duration,
),
BuffKind::Ensnared => (
vec![BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength))],
data.duration,
),
BuffKind::Hastened => (
vec![
BuffEffect::MovementSpeed(1.0 + data.strength), BuffEffect::MovementSpeed(1.0 + data.strength),
BuffEffect::AttackSpeed(1.0 + data.strength), BuffEffect::AttackSpeed(1.0 + data.strength),
], ],
data.duration, BuffKind::Fortitude => vec![BuffEffect::PoiseReduction(data.strength)],
), BuffKind::Parried => vec![BuffEffect::AttackSpeed(0.5)],
BuffKind::Fortitude => ( BuffKind::PotionSickness => vec![BuffEffect::HealReduction(data.strength)],
vec![BuffEffect::PoiseReduction(data.strength)], };
data.duration, let start_time = Time(time.0 + data.delay.map_or(0.0, |delay| delay.0));
), let end_time = if cat_ids
BuffKind::Parried => (vec![BuffEffect::AttackSpeed(0.5)], data.duration), .iter()
BuffKind::PotionSickness => ( .any(|cat_id| matches!(cat_id, BuffCategory::FromActiveAura(..)))
vec![BuffEffect::HealReduction { {
rate: data.strength, None
}], } else {
data.duration, data.duration.map(|dur| Time(start_time.0 + dur.0))
),
}; };
Buff { Buff {
kind, kind,
data, data,
cat_ids, cat_ids,
time, start_time,
delay: data.delay, end_time,
effects, effects,
source, source,
} }
} }
/// Calculate how much time has elapsed since the buff was applied (if the /// Calculate how much time has elapsed since the buff was applied
/// buff has a finite duration, otherwise insufficient information pub fn elapsed(&self, time: Time) -> Secs { Secs(time.0 - self.start_time.0) }
/// exists to track that)
pub fn elapsed(&self) -> Option<Duration> {
self.data.duration.zip_with(self.time, |x, y| x - y)
}
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -454,9 +402,13 @@ impl PartialOrd for Buff {
Some(Ordering::Greater) Some(Ordering::Greater)
} else if self.data.strength < other.data.strength { } else if self.data.strength < other.data.strength {
Some(Ordering::Less) Some(Ordering::Less)
} else if compare_duration(self.time, other.time) { } else if self.data.delay.is_none() && other.data.delay.is_some() {
Some(Ordering::Greater) Some(Ordering::Greater)
} else if compare_duration(other.time, self.time) { } else if self.data.delay.is_some() && other.data.delay.is_none() {
Some(Ordering::Less)
} else if compare_end_time(self.end_time, other.end_time) {
Some(Ordering::Greater)
} else if compare_end_time(other.end_time, self.end_time) {
Some(Ordering::Less) Some(Ordering::Less)
} else { } else {
None None
@ -465,14 +417,16 @@ impl PartialOrd for Buff {
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn compare_duration(a: Option<Duration>, b: Option<Duration>) -> bool { fn compare_end_time(a: Option<Time>, b: Option<Time>) -> bool {
a.map_or(true, |dur_a| b.map_or(false, |dur_b| dur_a > dur_b)) a.map_or(true, |time_a| b.map_or(false, |time_b| time_a.0 > time_b.0))
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
impl PartialEq for Buff { impl PartialEq for Buff {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.data.strength == other.data.strength && self.time == other.time self.data.strength == other.data.strength
&& self.end_time == other.end_time
&& self.start_time == other.start_time
} }
} }
@ -509,8 +463,10 @@ pub enum BuffSource {
pub struct Buffs { pub struct Buffs {
/// Uid used for synchronization /// Uid used for synchronization
id_counter: u64, id_counter: u64,
/// Maps Kinds of buff to Id's of currently applied buffs of that kind /// Maps Kinds of buff to Id's of currently applied buffs of that kind and
pub kinds: HashMap<BuffKind, Vec<BuffId>>, /// the time that the first buff was added (time gets reset if entity no
/// longer has buffs of that kind)
pub kinds: HashMap<BuffKind, (Vec<BuffId>, Time)>,
// All currently applied buffs stored by Id // All currently applied buffs stored by Id
pub buffs: HashMap<BuffId, Buff>, pub buffs: HashMap<BuffId, Buff>,
} }
@ -519,13 +475,14 @@ pub struct Buffs {
impl Buffs { impl Buffs {
fn sort_kind(&mut self, kind: BuffKind) { fn sort_kind(&mut self, kind: BuffKind) {
if let Some(buff_order) = self.kinds.get_mut(&kind) { if let Some(buff_order) = self.kinds.get_mut(&kind) {
if buff_order.is_empty() { if buff_order.0.is_empty() {
self.kinds.remove(&kind); self.kinds.remove(&kind);
} else { } else {
let buffs = &self.buffs; let buffs = &self.buffs;
// Intentionally sorted in reverse so that the strongest buffs are earlier in // Intentionally sorted in reverse so that the strongest buffs are earlier in
// the vector // the vector
buff_order buff_order
.0
.sort_by(|a, b| buffs[b].partial_cmp(&buffs[a]).unwrap_or(Ordering::Equal)); .sort_by(|a, b| buffs[b].partial_cmp(&buffs[a]).unwrap_or(Ordering::Equal));
} }
} }
@ -533,24 +490,31 @@ impl Buffs {
pub fn remove_kind(&mut self, kind: BuffKind) { pub fn remove_kind(&mut self, kind: BuffKind) {
if let Some(buff_ids) = self.kinds.get_mut(&kind) { if let Some(buff_ids) = self.kinds.get_mut(&kind) {
for id in buff_ids { for id in &buff_ids.0 {
self.buffs.remove(id); self.buffs.remove(id);
} }
self.kinds.remove(&kind); self.kinds.remove(&kind);
} }
} }
pub fn force_insert(&mut self, id: BuffId, buff: Buff) -> BuffId { fn force_insert(&mut self, id: BuffId, buff: Buff, current_time: Time) -> BuffId {
let kind = buff.kind; let kind = buff.kind;
self.kinds.entry(kind).or_default().push(id); self.kinds
.entry(kind)
.or_insert((Vec::new(), current_time))
.0
.push(id);
self.buffs.insert(id, buff); self.buffs.insert(id, buff);
self.sort_kind(kind); self.sort_kind(kind);
if kind.queues() {
self.delay_queueable_buffs(kind, current_time);
}
id id
} }
pub fn insert(&mut self, buff: Buff) -> BuffId { pub fn insert(&mut self, buff: Buff, current_time: Time) -> BuffId {
self.id_counter += 1; self.id_counter += 1;
self.force_insert(self.id_counter, buff) self.force_insert(self.id_counter, buff, current_time)
} }
pub fn contains(&self, kind: BuffKind) -> bool { self.kinds.contains_key(&kind) } pub fn contains(&self, kind: BuffKind) -> bool { self.kinds.contains_key(&kind) }
@ -559,7 +523,7 @@ impl Buffs {
pub fn iter_kind(&self, kind: BuffKind) -> impl Iterator<Item = (BuffId, &Buff)> + '_ { pub fn iter_kind(&self, kind: BuffKind) -> impl Iterator<Item = (BuffId, &Buff)> + '_ {
self.kinds self.kinds
.get(&kind) .get(&kind)
.map(|ids| ids.iter()) .map(|ids| ids.0.iter())
.unwrap_or_else(|| [].iter()) .unwrap_or_else(|| [].iter())
.map(move |id| (*id, &self.buffs[id])) .map(move |id| (*id, &self.buffs[id]))
} }
@ -571,29 +535,52 @@ impl Buffs {
if kind.stacks() { if kind.stacks() {
// Iterate stackable buffs in reverse order to show the timer of the soonest one // Iterate stackable buffs in reverse order to show the timer of the soonest one
// to expire // to expire
Either::Left(ids.iter().filter_map(|id| self.buffs.get(id)).rev()) Either::Left(ids.0.iter().filter_map(|id| self.buffs.get(id)).rev())
} else { } else {
Either::Right(self.buffs.get(&ids[0]).into_iter()) Either::Right(self.buffs.get(&ids.0[0]).into_iter())
} }
}) })
} }
// Gets most powerful buff of a given kind // Gets most powerful buff of a given kind
// pub fn get_active_kind(&self, kind: BuffKind) -> Buff
pub fn remove(&mut self, buff_id: BuffId) { pub fn remove(&mut self, buff_id: BuffId) {
if let Some(kind) = self.buffs.remove(&buff_id) { if let Some(kind) = self.buffs.remove(&buff_id) {
let kind = kind.kind; let kind = kind.kind;
self.kinds self.kinds
.get_mut(&kind) .get_mut(&kind)
.map(|ids| ids.retain(|id| *id != buff_id)); .map(|ids| ids.0.retain(|id| *id != buff_id));
self.sort_kind(kind); self.sort_kind(kind);
} }
} }
/// Returns an immutable reference to the buff kinds on an entity, and a fn delay_queueable_buffs(&mut self, kind: BuffKind, current_time: Time) {
/// mutable reference to the buffs let mut next_start_time: Option<Time> = None;
pub fn parts(&mut self) -> (&HashMap<BuffKind, Vec<BuffId>>, &mut HashMap<BuffId, Buff>) { debug_assert!(kind.queues());
(&self.kinds, &mut self.buffs) if let Some(buffs) = self.kinds.get(&kind) {
buffs.0.iter().for_each(|id| {
if let Some(buff) = self.buffs.get_mut(id) {
// End time only being updated when there is some next_start_time will
// technically cause buffs to "end early" if they have a weaker strength than a
// buff with an infinite duration, but this is fine since those buffs wouldn't
// matter anyways
if let Some(next_start_time) = next_start_time {
// Delays buff so that it has the same progress it has now at the time the
// previous buff would end.
//
// Shift should be relative to current time, unless the buff is delayed and
// hasn't started yet
let reference_time = current_time.0.max(buff.start_time.0);
// If buff has a delay, ensure that queueables shuffling queue does not
// potentially allow skipping delay
buff.start_time = Time(next_start_time.0.max(buff.start_time.0));
buff.end_time = buff.end_time.map(|end| {
Time(end.0 + next_start_time.0.max(reference_time) - reference_time)
});
}
next_start_time = buff.end_time;
}
})
}
} }
} }
@ -603,3 +590,129 @@ pub type BuffId = u64;
impl Component for Buffs { impl Component for Buffs {
type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>; type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
} }
#[cfg(test)]
pub mod tests {
use crate::comp::buff::*;
#[cfg(test)]
fn create_test_queueable_buff(buff_data: BuffData, time: Time) -> Buff {
// Change to another buff that queues if we ever add one and remove saturation,
// otherwise maybe add a test buff kind?
debug_assert!(BuffKind::Saturation.queues());
Buff::new(
BuffKind::Saturation,
buff_data,
Vec::new(),
BuffSource::Unknown,
time,
None,
)
}
#[test]
/// Tests a number of buffs with various progresses that queue to ensure
/// queue has correct total duration
fn test_queueable_buffs_three() {
let mut buff_comp: Buffs = Default::default();
let buff_data = BuffData::new(1.0, Some(Secs(10.0)), None);
let time_a = Time(0.0);
buff_comp.insert(create_test_queueable_buff(buff_data, time_a), time_a);
let time_b = Time(6.0);
buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
let time_c = Time(11.0);
buff_comp.insert(create_test_queueable_buff(buff_data, time_c), time_c);
// Check that all buffs have an end_time less than or equal to 30, and that at
// least one has an end_time greater than or equal to 30.
//
// This should be true because 3 buffs that each lasted for 10 seconds were
// inserted at various times, so the total duration should be 30 seconds.
assert!(
buff_comp
.buffs
.values()
.all(|b| b.end_time.unwrap().0 < 30.01)
);
assert!(
buff_comp
.buffs
.values()
.any(|b| b.end_time.unwrap().0 > 29.99)
);
}
#[test]
/// Tests that if a buff had a delay but will start soon, and an immediate
/// queueable buff is added, delayed buff has correct start time
fn test_queueable_buff_delay_start() {
let mut buff_comp: Buffs = Default::default();
let queued_buff_data = BuffData::new(1.0, Some(Secs(10.0)), Some(Secs(10.0)));
let buff_data = BuffData::new(1.0, Some(Secs(10.0)), None);
let time_a = Time(0.0);
buff_comp.insert(create_test_queueable_buff(queued_buff_data, time_a), time_a);
let time_b = Time(6.0);
buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
// Check that all buffs have an end_time less than or equal to 26, and that at
// least one has an end_time greater than or equal to 26.
//
// This should be true because the first buff added had a delay of 10 seconds
// and a duration of 10 seconds, the second buff added at 6 seconds had no
// delay, and a duration of 10 seconds. When it finishes at 16 seconds the first
// buff is past the delay time so should finish at 26 seconds.
assert!(
buff_comp
.buffs
.values()
.all(|b| b.end_time.unwrap().0 < 26.01)
);
assert!(
buff_comp
.buffs
.values()
.any(|b| b.end_time.unwrap().0 > 25.99)
);
}
#[test]
/// Tests that if a buff had a long delay, a short immediate queueable buff
/// does not move delayed buff start or end times
fn test_queueable_buff_long_delay() {
let mut buff_comp: Buffs = Default::default();
let queued_buff_data = BuffData::new(1.0, Some(Secs(10.0)), Some(Secs(50.0)));
let buff_data = BuffData::new(1.0, Some(Secs(10.0)), None);
let time_a = Time(0.0);
buff_comp.insert(create_test_queueable_buff(queued_buff_data, time_a), time_a);
let time_b = Time(10.0);
buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
// Check that all buffs have either an end time less than or equal to 20 seconds
// XOR a start time greater than or equal to 50 seconds, that all buffs have a
// start time less than or equal to 50 seconds, that all buffs have an end time
// less than or equal to 60 seconds, and that at least one buff has an end time
// greater than or equal to 60 seconds
//
// This should be true because the first buff has a delay of 50 seconds, the
// second buff added has no delay at 10 seconds and lasts 10 seconds, so should
// end at 20 seconds and not affect the start time of the delayed buff, and
// since the delayed buff was not affected the end time should be 10 seconds
// after the start time: 60 seconds != used here to emulate xor
assert!(
buff_comp
.buffs
.values()
.all(|b| (b.end_time.unwrap().0 < 20.01) != (b.start_time.0 > 49.99))
);
assert!(buff_comp.buffs.values().all(|b| b.start_time.0 < 50.01));
assert!(
buff_comp
.buffs
.values()
.all(|b| b.end_time.unwrap().0 < 60.01)
);
assert!(
buff_comp
.buffs
.values()
.any(|b| b.end_time.unwrap().0 > 59.99)
);
}
}

View File

@ -3,6 +3,7 @@ use crate::comp::Pos;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use specs::Entity; use specs::Entity;
use std::ops::{Mul, MulAssign};
/// A resource that stores the time of day. /// A resource that stores the time of day.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
@ -16,6 +17,20 @@ pub struct Time(pub f64);
#[derive(Default)] #[derive(Default)]
pub struct DeltaTime(pub f32); pub struct DeltaTime(pub f32);
/// A resource used to indicate a duration of time, in seconds
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[serde(transparent)]
pub struct Secs(pub f64);
impl Mul<f64> for Secs {
type Output = Self;
fn mul(self, mult: f64) -> Self { Self(self.0 * mult) }
}
impl MulAssign<f64> for Secs {
fn mul_assign(&mut self, mult: f64) { *self = *self * mult; }
}
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[derive(Default)] #[derive(Default)]
pub struct EntitiesDiedLastTick(pub Vec<(Entity, Pos)>); pub struct EntitiesDiedLastTick(pub Vec<(Entity, Pos)>);

View File

@ -6,6 +6,7 @@ use crate::{
CharacterState, StateUpdate, CharacterState, StateUpdate,
}, },
event::ServerEvent, event::ServerEvent,
resources::Secs,
states::{ states::{
behavior::{CharacterBehavior, JoinData}, behavior::{CharacterBehavior, JoinData},
utils::*, utils::*,
@ -28,7 +29,7 @@ pub struct StaticData {
/// Has information used to construct the auras /// Has information used to construct the auras
pub auras: Vec<AuraBuffConstructor>, pub auras: Vec<AuraBuffConstructor>,
/// How long aura lasts /// How long aura lasts
pub aura_duration: Duration, pub aura_duration: Secs,
/// Radius of aura /// Radius of aura
pub range: f32, pub range: f32,
/// What key is used to press ability /// What key is used to press ability
@ -79,6 +80,7 @@ impl CharacterBehavior for Data {
self.static_data.range, self.static_data.range,
Some(self.static_data.aura_duration), Some(self.static_data.aura_duration),
targets, targets,
*data.time,
); );
if self.static_data.scales_with_combo { if self.static_data.scales_with_combo {
match aura.aura_kind { match aura.aura_kind {

View File

@ -9,7 +9,7 @@ use crate::{
}, },
link::Is, link::Is,
mounting::Rider, mounting::Rider,
resources::DeltaTime, resources::{DeltaTime, Time},
terrain::TerrainGrid, terrain::TerrainGrid,
uid::Uid, uid::Uid,
}; };
@ -125,6 +125,7 @@ pub struct JoinData<'a> {
pub mass: &'a Mass, pub mass: &'a Mass,
pub density: &'a Density, pub density: &'a Density,
pub dt: &'a DeltaTime, pub dt: &'a DeltaTime,
pub time: &'a Time,
pub controller: &'a Controller, pub controller: &'a Controller,
pub inputs: &'a ControllerInputs, pub inputs: &'a ControllerInputs,
pub health: Option<&'a Health>, pub health: Option<&'a Health>,
@ -176,6 +177,7 @@ impl<'a> JoinData<'a> {
j: &'a JoinStruct<'a>, j: &'a JoinStruct<'a>,
updater: &'a LazyUpdate, updater: &'a LazyUpdate,
dt: &'a DeltaTime, dt: &'a DeltaTime,
time: &'a Time,
msm: &'a MaterialStatManifest, msm: &'a MaterialStatManifest,
ability_map: &'a AbilityMap, ability_map: &'a AbilityMap,
) -> Self { ) -> Self {
@ -200,6 +202,7 @@ impl<'a> JoinData<'a> {
skill_set: j.skill_set, skill_set: j.skill_set,
updater, updater,
dt, dt,
time,
msm, msm,
ability_map, ability_map,
combo: j.combo, combo: j.combo,

View File

@ -5,6 +5,7 @@ use crate::{
CharacterState, StateUpdate, CharacterState, StateUpdate,
}, },
event::ServerEvent, event::ServerEvent,
resources::Secs,
states::{ states::{
behavior::{CharacterBehavior, JoinData}, behavior::{CharacterBehavior, JoinData},
utils::*, utils::*,
@ -27,7 +28,7 @@ pub struct StaticData {
/// Strength of the created buff /// Strength of the created buff
pub buff_strength: f32, pub buff_strength: f32,
/// How long buff lasts /// How long buff lasts
pub buff_duration: Option<Duration>, pub buff_duration: Option<Secs>,
/// What key is used to press ability /// What key is used to press ability
pub ability_info: AbilityInfo, pub ability_info: AbilityInfo,
} }
@ -69,6 +70,8 @@ impl CharacterBehavior for Data {
}, },
Vec::new(), Vec::new(),
BuffSource::Character { by: *data.uid }, BuffSource::Character { by: *data.uid },
*data.time,
Some(data.stats),
); );
output_events.emit_server(ServerEvent::Buff { output_events.emit_server(ServerEvent::Buff {
entity: data.entity, entity: data.entity,

View File

@ -75,6 +75,7 @@ impl BlockChange {
pub struct ScheduledBlockChange { pub struct ScheduledBlockChange {
changes: TimerQueue<HashMap<Vec3<i32>, Block>>, changes: TimerQueue<HashMap<Vec3<i32>, Block>>,
outcomes: TimerQueue<HashMap<Vec3<i32>, Block>>, outcomes: TimerQueue<HashMap<Vec3<i32>, Block>>,
last_poll_time: u64,
} }
impl ScheduledBlockChange { impl ScheduledBlockChange {
pub fn set(&mut self, pos: Vec3<i32>, block: Block, replace_time: f64) { pub fn set(&mut self, pos: Vec3<i32>, block: Block, replace_time: f64) {
@ -252,9 +253,9 @@ impl State {
ecs.insert(TimeOfDay(0.0)); ecs.insert(TimeOfDay(0.0));
ecs.insert(Calendar::default()); ecs.insert(Calendar::default());
ecs.insert(WeatherGrid::new(Vec2::zero())); ecs.insert(WeatherGrid::new(Vec2::zero()));
ecs.insert(Time(0.0));
// Register unsynced resources used by the ECS. // Register unsynced resources used by the ECS.
ecs.insert(Time(0.0));
ecs.insert(DeltaTime(0.0)); ecs.insert(DeltaTime(0.0));
ecs.insert(PlayerEntity(None)); ecs.insert(PlayerEntity(None));
ecs.insert(TerrainGrid::new(map_size_lg, default_chunk).unwrap()); ecs.insert(TerrainGrid::new(map_size_lg, default_chunk).unwrap());
@ -546,11 +547,18 @@ impl State {
let mut scheduled_changes = self.ecs.write_resource::<ScheduledBlockChange>(); let mut scheduled_changes = self.ecs.write_resource::<ScheduledBlockChange>();
let current_time: f64 = self.ecs.read_resource::<Time>().0 * SECONDS_TO_MILLISECONDS; let current_time: f64 = self.ecs.read_resource::<Time>().0 * SECONDS_TO_MILLISECONDS;
while let Some(changes) = scheduled_changes.changes.poll(current_time as u64) { let current_time = current_time as u64;
// This is important as the poll function has a debug assert that the new poll
// is at a more recent time than the old poll. As Time is synced between server
// and client, there is a chance that client dt can get slightly ahead of a
// server update, so we do not want to panic in that scenario.
if scheduled_changes.last_poll_time < current_time {
scheduled_changes.last_poll_time = current_time;
while let Some(changes) = scheduled_changes.changes.poll(current_time) {
modified_blocks.extend(changes.iter()); modified_blocks.extend(changes.iter());
} }
let outcome = self.ecs.read_resource::<EventBus<Outcome>>(); let outcome = self.ecs.read_resource::<EventBus<Outcome>>();
while let Some(outcomes) = scheduled_changes.outcomes.poll(current_time as u64) { while let Some(outcomes) = scheduled_changes.outcomes.poll(current_time) {
for (pos, block) in outcomes.iter() { for (pos, block) in outcomes.iter() {
let offset_dir = Vec3::<i32>::zero() - pos; let offset_dir = Vec3::<i32>::zero() - pos;
let offset = offset_dir let offset = offset_dir
@ -565,6 +573,7 @@ impl State {
} }
} }
} }
}
// Apply block modifications // Apply block modifications
// Only include in `TerrainChanges` if successful // Only include in `TerrainChanges` if successful
modified_blocks.retain(|pos, block| { modified_blocks.retain(|pos, block| {

View File

@ -4,24 +4,23 @@ use common::{
aura::{AuraChange, AuraKey, AuraKind, AuraTarget}, aura::{AuraChange, AuraKey, AuraKind, AuraTarget},
buff::{Buff, BuffCategory, BuffChange, BuffSource}, buff::{Buff, BuffCategory, BuffChange, BuffSource},
group::Group, group::Group,
Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Stats,
}, },
event::{Emitter, EventBus, ServerEvent}, event::{Emitter, EventBus, ServerEvent},
resources::DeltaTime, resources::Time,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use specs::{ use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, Read,
ReadStorage, SystemData, World, WriteStorage, ReadStorage, SystemData, World,
}; };
use std::time::Duration;
#[derive(SystemData)] #[derive(SystemData)]
pub struct ReadData<'a> { pub struct ReadData<'a> {
entities: Entities<'a>, entities: Entities<'a>,
players: ReadStorage<'a, Player>, players: ReadStorage<'a, Player>,
dt: Read<'a, DeltaTime>, time: Read<'a, Time>,
server_bus: Read<'a, EventBus<ServerEvent>>, server_bus: Read<'a, EventBus<ServerEvent>>,
uid_allocator: Read<'a, UidAllocator>, uid_allocator: Read<'a, UidAllocator>,
cached_spatial_grid: Read<'a, common::CachedSpatialGrid>, cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
@ -31,57 +30,38 @@ pub struct ReadData<'a> {
healths: ReadStorage<'a, Health>, healths: ReadStorage<'a, Health>,
groups: ReadStorage<'a, Group>, groups: ReadStorage<'a, Group>,
uids: ReadStorage<'a, Uid>, uids: ReadStorage<'a, Uid>,
stats: ReadStorage<'a, Stats>,
buffs: ReadStorage<'a, Buffs>,
auras: ReadStorage<'a, Auras>,
} }
#[derive(Default)] #[derive(Default)]
pub struct Sys; pub struct Sys;
impl<'a> System<'a> for Sys { impl<'a> System<'a> for Sys {
type SystemData = ( type SystemData = ReadData<'a>;
ReadData<'a>,
WriteStorage<'a, Auras>,
WriteStorage<'a, Buffs>,
);
const NAME: &'static str = "aura"; const NAME: &'static str = "aura";
const ORIGIN: Origin = Origin::Common; const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create; const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, (read_data, mut auras, mut buffs): Self::SystemData) { fn run(_job: &mut Job<Self>, read_data: Self::SystemData) {
let mut server_emitter = read_data.server_bus.emitter(); let mut server_emitter = read_data.server_bus.emitter();
let dt = read_data.dt.0;
auras.set_event_emission(false);
buffs.set_event_emission(false);
// Iterate through all buffs, on any buffs that are from an aura, sets the check
// for whether the buff recently set by aura to false
for (_, mut buffs_comp) in (&read_data.entities, &mut buffs).join() {
for (_, buff) in buffs_comp.buffs.iter_mut() {
if let Some(cat_id) = buff
.cat_ids
.iter_mut()
.find(|cat_id| matches!(cat_id, BuffCategory::FromAura(true)))
{
*cat_id = BuffCategory::FromAura(false);
}
}
}
// Iterate through all entities with an aura // Iterate through all entities with an aura
for (entity, pos, mut auras_comp) in for (entity, pos, auras_comp, uid) in (
(&read_data.entities, &read_data.positions, &mut auras).join() &read_data.entities,
&read_data.positions,
&read_data.auras,
&read_data.uids,
)
.join()
{ {
let mut expired_auras = Vec::<AuraKey>::new(); let mut expired_auras = Vec::<AuraKey>::new();
// Iterate through the auras attached to this entity // Iterate through the auras attached to this entity
for (key, aura) in auras_comp.auras.iter_mut() { for (key, aura) in auras_comp.auras.iter() {
// Tick the aura and subtract dt from it // Tick the aura and subtract dt from it
if let Some(remaining_time) = &mut aura.duration { if let Some(end_time) = aura.end_time {
if let Some(new_duration) = if read_data.time.0 > end_time.0 {
remaining_time.checked_sub(Duration::from_secs_f32(dt))
{
*remaining_time = new_duration;
} else {
*remaining_time = Duration::default();
expired_auras.push(key); expired_auras.push(key);
} }
} }
@ -96,11 +76,17 @@ impl<'a> System<'a> for Sys {
.and_then(|l| read_data.healths.get(target).map(|r| (l, r))) .and_then(|l| read_data.healths.get(target).map(|r| (l, r)))
.and_then(|l| read_data.uids.get(target).map(|r| (l, r))) .and_then(|l| read_data.uids.get(target).map(|r| (l, r)))
.map(|((target_pos, health), target_uid)| { .map(|((target_pos, health), target_uid)| {
(target, target_pos, health, target_uid) (
target,
target_pos,
health,
target_uid,
read_data.stats.get(target),
)
}) })
}); });
target_iter.for_each(|(target, target_pos, health, target_uid)| { target_iter.for_each(|(target, target_pos, health, target_uid, stats)| {
let mut target_buffs = match buffs.get_mut(target) { let target_buffs = match read_data.buffs.get(target) {
Some(buff) => buff, Some(buff) => buff,
None => return, None => return,
}; };
@ -127,10 +113,13 @@ impl<'a> System<'a> for Sys {
if is_target { if is_target {
activate_aura( activate_aura(
key,
aura, aura,
*uid,
target, target,
health, health,
&mut target_buffs, target_buffs,
stats,
&read_data, &read_data,
&mut server_emitter, &mut server_emitter,
); );
@ -145,18 +134,19 @@ impl<'a> System<'a> for Sys {
}); });
} }
} }
auras.set_event_emission(true);
buffs.set_event_emission(true);
} }
} }
#[warn(clippy::pedantic)] #[warn(clippy::pedantic)]
//#[warn(clippy::nursery)] //#[warn(clippy::nursery)]
fn activate_aura( fn activate_aura(
key: AuraKey,
aura: &Aura, aura: &Aura,
applier: Uid,
target: EcsEntity, target: EcsEntity,
health: &Health, health: &Health,
target_buffs: &mut Buffs, target_buffs: &Buffs,
stats: Option<&Stats>,
read_data: &ReadData, read_data: &ReadData,
server_emitter: &mut Emitter<ServerEvent>, server_emitter: &mut Emitter<ServerEvent>,
) { ) {
@ -237,12 +227,9 @@ fn activate_aura(
let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| { let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
buff.cat_ids buff.cat_ids
.iter() .iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromAura(_))) .any(|cat_id| matches!(cat_id, BuffCategory::FromActiveAura(uid, aura_key) if *aura_key == key && *uid == applier))
&& buff.kind == kind && buff.kind == kind
&& buff.data.strength >= data.strength && buff.data.strength >= data.strength
&& buff.time.map_or(true, |dur| {
data.duration.map_or(false, |dur_2| dur >= dur_2)
})
}); });
if emit_buff { if emit_buff {
server_emitter.emit(ServerEvent::Buff { server_emitter.emit(ServerEvent::Buff {
@ -250,29 +237,13 @@ fn activate_aura(
buff_change: BuffChange::Add(Buff::new( buff_change: BuffChange::Add(Buff::new(
kind, kind,
data, data,
vec![category, BuffCategory::FromAura(true)], vec![category, BuffCategory::FromActiveAura(applier, key)],
source, source,
*read_data.time,
stats,
)), )),
}); });
} }
// Finds all buffs on target that are from an aura, are of
// the same buff kind, and are of at most the same strength.
// For any such buffs, marks it as recently applied.
for (_, buff) in target_buffs.buffs.iter_mut().filter(|(_, buff)| {
buff.cat_ids
.iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromAura(_)))
&& buff.kind == kind
&& buff.data.strength <= data.strength
}) {
if let Some(cat_id) = buff
.cat_ids
.iter_mut()
.find(|cat_id| matches!(cat_id, BuffCategory::FromAura(false)))
{
*cat_id = BuffCategory::FromAura(true);
}
}
}, },
} }
} }

View File

@ -1,6 +1,7 @@
use common::{ use common::{
combat::DamageContributor, combat::DamageContributor,
comp::{ comp::{
aura::Auras,
body::{object, Body}, body::{object, Body},
buff::{ buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource,
@ -9,23 +10,21 @@ use common::{
fluid_dynamics::{Fluid, LiquidKind}, fluid_dynamics::{Fluid, LiquidKind},
item::MaterialStatManifest, item::MaterialStatManifest,
Energy, Group, Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Energy, Group, Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState,
Stats, Pos, Stats,
}, },
event::{Emitter, EventBus, ServerEvent}, event::{Emitter, EventBus, ServerEvent},
resources::{DeltaTime, Time}, resources::{DeltaTime, Secs, Time},
terrain::SpriteKind, terrain::SpriteKind,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
Damage, DamageSource, Damage, DamageSource,
}; };
use common_base::prof_span; use common_base::prof_span;
use common_ecs::{Job, Origin, ParMode, Phase, System}; use common_ecs::{Job, Origin, ParMode, Phase, System};
use hashbrown::HashMap;
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use specs::{ use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Entity, Join, ParJoin, Read, saveload::MarkerAllocator, shred::ResourceId, Entities, Entity, Join, ParJoin, Read,
ReadExpect, ReadStorage, SystemData, World, WriteStorage, ReadExpect, ReadStorage, SystemData, World, WriteStorage,
}; };
use std::time::Duration;
#[derive(SystemData)] #[derive(SystemData)]
pub struct ReadData<'a> { pub struct ReadData<'a> {
@ -40,6 +39,9 @@ pub struct ReadData<'a> {
uid_allocator: Read<'a, UidAllocator>, uid_allocator: Read<'a, UidAllocator>,
time: Read<'a, Time>, time: Read<'a, Time>,
msm: ReadExpect<'a, MaterialStatManifest>, msm: ReadExpect<'a, MaterialStatManifest>,
buffs: ReadStorage<'a, Buffs>,
auras: ReadStorage<'a, Auras>,
positions: ReadStorage<'a, Pos>,
} }
#[derive(Default)] #[derive(Default)]
@ -47,7 +49,6 @@ pub struct Sys;
impl<'a> System<'a> for Sys { impl<'a> System<'a> for Sys {
type SystemData = ( type SystemData = (
ReadData<'a>, ReadData<'a>,
WriteStorage<'a, Buffs>,
WriteStorage<'a, Stats>, WriteStorage<'a, Stats>,
WriteStorage<'a, Body>, WriteStorage<'a, Body>,
WriteStorage<'a, LightEmitter>, WriteStorage<'a, LightEmitter>,
@ -59,12 +60,11 @@ impl<'a> System<'a> for Sys {
fn run( fn run(
job: &mut Job<Self>, job: &mut Job<Self>,
(read_data, mut buffs, mut stats, mut bodies, mut light_emitters): Self::SystemData, (read_data, mut stats, mut bodies, mut light_emitters): Self::SystemData,
) { ) {
let mut server_emitter = read_data.server_bus.emitter(); let mut server_emitter = read_data.server_bus.emitter();
let dt = read_data.dt.0; let dt = read_data.dt.0;
// Set to false to avoid spamming server // Set to false to avoid spamming server
buffs.set_event_emission(false);
stats.set_event_emission(false); stats.set_event_emission(false);
// Put out underwater campfires. Logically belongs here since this system also // Put out underwater campfires. Logically belongs here since this system also
@ -125,9 +125,9 @@ impl<'a> System<'a> for Sys {
} }
} }
for (entity, mut buff_comp, mut stat, health, energy, physics_state) in ( for (entity, buff_comp, mut stat, health, energy, physics_state) in (
&read_data.entities, &read_data.entities,
&mut buffs, &read_data.buffs,
&mut stats, &mut stats,
&read_data.healths, &read_data.healths,
&read_data.energies, &read_data.energies,
@ -146,9 +146,11 @@ impl<'a> System<'a> for Sys {
entity, entity,
buff_change: BuffChange::Add(Buff::new( buff_change: BuffChange::Add(Buff::new(
BuffKind::Ensnared, BuffKind::Ensnared,
BuffData::new(1.0, Some(Duration::from_secs_f32(1.0)), None), BuffData::new(1.0, Some(Secs(1.0)), None),
Vec::new(), Vec::new(),
BuffSource::World, BuffSource::World,
*read_data.time,
Some(&stat),
)), )),
}); });
} }
@ -161,9 +163,11 @@ impl<'a> System<'a> for Sys {
entity, entity,
buff_change: BuffChange::Add(Buff::new( buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding, BuffKind::Bleeding,
BuffData::new(1.0, Some(Duration::from_secs_f32(6.0)), None), BuffData::new(1.0, Some(Secs(6.0)), None),
Vec::new(), Vec::new(),
BuffSource::World, BuffSource::World,
*read_data.time,
Some(&stat),
)), )),
}); });
} }
@ -176,9 +180,11 @@ impl<'a> System<'a> for Sys {
entity, entity,
buff_change: BuffChange::Add(Buff::new( buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding, BuffKind::Bleeding,
BuffData::new(15.0, Some(Duration::from_secs_f32(0.1)), None), BuffData::new(15.0, Some(Secs(0.1)), None),
Vec::new(), Vec::new(),
BuffSource::World, BuffSource::World,
*read_data.time,
Some(&stat),
)), )),
}); });
// When standing on IceSpike also apply Frozen // When standing on IceSpike also apply Frozen
@ -186,9 +192,11 @@ impl<'a> System<'a> for Sys {
entity, entity,
buff_change: BuffChange::Add(Buff::new( buff_change: BuffChange::Add(Buff::new(
BuffKind::Frozen, BuffKind::Frozen,
BuffData::new(0.2, Some(Duration::from_secs_f32(1.0)), None), BuffData::new(0.2, Some(Secs(1.0)), None),
Vec::new(), Vec::new(),
BuffSource::World, BuffSource::World,
*read_data.time,
Some(&stat),
)), )),
}); });
} }
@ -207,6 +215,8 @@ impl<'a> System<'a> for Sys {
BuffData::new(20.0, None, None), BuffData::new(20.0, None, None),
vec![BuffCategory::Natural], vec![BuffCategory::Natural],
BuffSource::World, BuffSource::World,
*read_data.time,
Some(&stat),
)), )),
}); });
} else if matches!( } else if matches!(
@ -225,31 +235,77 @@ impl<'a> System<'a> for Sys {
} }
} }
let (buff_comp_kinds, buff_comp_buffs): (
&HashMap<BuffKind, Vec<BuffId>>,
&mut HashMap<BuffId, Buff>,
) = buff_comp.parts();
let mut expired_buffs = Vec::<BuffId>::new(); let mut expired_buffs = Vec::<BuffId>::new();
// For each buff kind present on entity, if the buff kind queues, only ticks // Replace buffs from an active aura with a normal buff when out of range of the
// duration of strongest buff of that kind, else it ticks durations of all buffs // aura
// of that kind. Any buffs whose durations expire are marked expired. buff_comp
for (kind, ids) in buff_comp_kinds.iter() { .buffs
if kind.queues() { .iter()
if let Some((Some(buff), id)) = .filter_map(|(id, buff)| {
ids.first().map(|id| (buff_comp_buffs.get_mut(id), id)) if let Some((uid, aura_key)) = buff.cat_ids.iter().find_map(|cat_id| {
if let BuffCategory::FromActiveAura(uid, aura_key) = cat_id {
Some((uid, aura_key))
} else {
None
}
}) {
Some((id, buff, uid, aura_key))
} else {
None
}
})
.for_each(|(buff_id, buff, uid, aura_key)| {
let replace = if let Some(aura_entity) = read_data
.uid_allocator
.retrieve_entity_internal((*uid).into())
{ {
tick_buff(*id, buff, dt, |id| expired_buffs.push(id)); if let Some(aura) = read_data
.auras
.get(aura_entity)
.and_then(|auras| auras.auras.get(*aura_key))
{
if let (Some(pos), Some(aura_pos)) = (
read_data.positions.get(entity),
read_data.positions.get(aura_entity),
) {
pos.0.distance_squared(aura_pos.0) > aura.radius.powi(2)
} else {
true
} }
} else { } else {
for (id, buff) in buff_comp_buffs true
.iter_mut()
.filter(|(i, _)| ids.iter().any(|id| id == *i))
{
tick_buff(*id, buff, dt, |id| expired_buffs.push(id));
} }
} else {
true
};
if replace {
expired_buffs.push(*buff_id);
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::Add(Buff::new(
buff.kind,
buff.data,
buff.cat_ids
.iter()
.copied()
.filter(|cat_id| {
!matches!(cat_id, BuffCategory::FromActiveAura(..))
})
.collect::<Vec<_>>(),
buff.source,
*read_data.time,
Some(&stat),
)),
});
} }
});
buff_comp.buffs.iter().for_each(|(id, buff)| {
if buff.end_time.map_or(false, |end| end.0 < read_data.time.0) {
expired_buffs.push(*id)
} }
});
let damage_reduction = Damage::compute_damage_reduction( let damage_reduction = Damage::compute_damage_reduction(
None, None,
@ -269,14 +325,13 @@ impl<'a> System<'a> for Sys {
stat.reset_temp_modifiers(); stat.reset_temp_modifiers();
// Iterator over the lists of buffs by kind // Iterator over the lists of buffs by kind
let buff_comp = &mut *buff_comp;
let mut buff_kinds = buff_comp let mut buff_kinds = buff_comp
.kinds .kinds
.iter() .iter()
.map(|(kind, ids)| (*kind, ids.clone())) .map(|(kind, ids)| (*kind, ids.clone()))
.collect::<Vec<(BuffKind, Vec<BuffId>)>>(); .collect::<Vec<(BuffKind, (Vec<BuffId>, Time))>>();
buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs()); buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
for (buff_kind, buff_ids) in buff_kinds.into_iter() { for (buff_kind, (buff_ids, kind_start_time)) in buff_kinds.into_iter() {
let mut active_buff_ids = Vec::new(); let mut active_buff_ids = Vec::new();
if buff_kind.stacks() { if buff_kind.stacks() {
// Process all the buffs of this kind // Process all the buffs of this kind
@ -286,9 +341,9 @@ impl<'a> System<'a> for Sys {
active_buff_ids.push(buff_ids[0]); active_buff_ids.push(buff_ids[0]);
} }
for buff_id in active_buff_ids.into_iter() { for buff_id in active_buff_ids.into_iter() {
if let Some(buff) = buff_comp.buffs.get_mut(&buff_id) { if let Some(buff) = buff_comp.buffs.get(&buff_id) {
// Skip the effect of buffs whose start delay hasn't expired. // Skip the effect of buffs whose start delay hasn't expired.
if buff.delay.is_some() { if buff.start_time.0 > read_data.time.0 {
continue; continue;
} }
// Get buff owner? // Get buff owner?
@ -299,11 +354,12 @@ impl<'a> System<'a> for Sys {
}; };
// Now, execute the buff, based on it's delta // Now, execute the buff, based on it's delta
for effect in &mut buff.effects { for effect in &buff.effects {
execute_effect( execute_effect(
effect, effect,
buff.kind, buff.kind,
buff.time, buff.start_time,
kind_start_time,
&read_data, &read_data,
&mut stat, &mut stat,
health, health,
@ -312,6 +368,8 @@ impl<'a> System<'a> for Sys {
buff_owner, buff_owner,
&mut server_emitter, &mut server_emitter,
dt, dt,
*read_data.time,
expired_buffs.contains(&buff_id),
); );
} }
} }
@ -339,15 +397,15 @@ impl<'a> System<'a> for Sys {
} }
} }
// Turned back to true // Turned back to true
buffs.set_event_emission(true);
stats.set_event_emission(true); stats.set_event_emission(true);
} }
} }
fn execute_effect( fn execute_effect(
effect: &mut BuffEffect, effect: &BuffEffect,
buff_kind: BuffKind, buff_kind: BuffKind,
buff_time: Option<std::time::Duration>, buff_start_time: Time,
buff_kind_start_time: Time,
read_data: &ReadData, read_data: &ReadData,
stat: &mut Stats, stat: &mut Stats,
health: &Health, health: &Health,
@ -356,28 +414,37 @@ fn execute_effect(
buff_owner: Option<Uid>, buff_owner: Option<Uid>,
server_emitter: &mut Emitter<ServerEvent>, server_emitter: &mut Emitter<ServerEvent>,
dt: f32, dt: f32,
time: Time,
buff_will_expire: bool,
) { ) {
match effect { match effect {
BuffEffect::HealthChangeOverTime { BuffEffect::HealthChangeOverTime {
rate, rate,
accumulated,
kind, kind,
instance, instance,
} => { } => {
*accumulated += *rate * dt; // Apply health change if buff wasn't just added, only once per second or when
// Apply health change only once per second, per health, or // buff is about to be removed
// when a buff is removed let time_passed = (time.0 - buff_start_time.0) as f32;
if accumulated.abs() > rate.abs().min(1.0) let one_second = (time_passed % 1.0) < dt;
|| buff_time.map_or(false, |dur| dur == Duration::default()) if time_passed > dt && (one_second || buff_will_expire) {
{ let amount = if one_second {
let (cause, by) = if *accumulated != 0.0 { // If buff not expiring this tick, then 1 second has passed
*rate
} else {
// If buff expiring this tick, only a fraction of the second has passed since
// last tick
((time.0 - buff_start_time.0) % 1.0) as f32 * rate
};
let (cause, by) = if amount != 0.0 {
(Some(DamageSource::Buff(buff_kind)), buff_owner) (Some(DamageSource::Buff(buff_kind)), buff_owner)
} else { } else {
(None, None) (None, None)
}; };
let mut amount = match *kind { let amount = match *kind {
ModifierKind::Additive => *accumulated, ModifierKind::Additive => amount,
ModifierKind::Fractional => health.maximum() * *accumulated, ModifierKind::Fractional => health.maximum() * amount,
}; };
let damage_contributor = by.and_then(|uid| { let damage_contributor = by.and_then(|uid| {
read_data read_data
@ -387,9 +454,6 @@ fn execute_effect(
DamageContributor::new(uid, read_data.groups.get(entity).cloned()) DamageContributor::new(uid, read_data.groups.get(entity).cloned())
}) })
}); });
if amount > 0.0 && matches!(buff_kind, BuffKind::Potion) {
amount *= stat.heal_multiplier;
}
server_emitter.emit(ServerEvent::HealthChange { server_emitter.emit(ServerEvent::HealthChange {
entity, entity,
change: HealthChange { change: HealthChange {
@ -401,29 +465,31 @@ fn execute_effect(
instance: *instance, instance: *instance,
}, },
}); });
*accumulated = 0.0;
}; };
}, },
BuffEffect::EnergyChangeOverTime { BuffEffect::EnergyChangeOverTime { rate, kind } => {
rate, // Apply energy change if buff wasn't just added, only once per second or when
accumulated, // buff is about to be removed
kind, let time_passed = (time.0 - buff_start_time.0) as f32;
} => { let one_second = (time_passed % 1.0) < dt;
*accumulated += *rate * dt; if time_passed > dt && (one_second || buff_will_expire) {
// Apply energy change only once per second, per energy, or let amount = if one_second {
// when a buff is removed // If buff not expiring this tick, then 1 second has passed
if accumulated.abs() > rate.abs().min(10.0) *rate
|| buff_time.map_or(false, |dur| dur == Duration::default()) } else {
{ // If buff expiring this tick, only a fraction of the second has passed since
// last tick
((time.0 - buff_start_time.0) % 1.0) as f32 * rate
};
let amount = match *kind { let amount = match *kind {
ModifierKind::Additive => *accumulated, ModifierKind::Additive => amount,
ModifierKind::Fractional => energy.maximum() * *accumulated, ModifierKind::Fractional => energy.maximum() * amount,
}; };
server_emitter.emit(ServerEvent::EnergyChange { server_emitter.emit(ServerEvent::EnergyChange {
entity, entity,
change: amount, change: amount,
}); });
*accumulated = 0.0;
}; };
}, },
BuffEffect::MaxHealthModifier { value, kind } => match kind { BuffEffect::MaxHealthModifier { value, kind } => match kind {
@ -450,35 +516,23 @@ fn execute_effect(
rate, rate,
kind, kind,
target_fraction, target_fraction,
achieved_fraction,
} => { } => {
// Current fraction uses information from last tick, which is let potential_amount = (time.0 - buff_kind_start_time.0) as f32 * rate;
// necessary as buffs from this tick are not guaranteed to have
// finished applying
let current_fraction = health.maximum() / health.base_max();
// If achieved_fraction not initialized, initialize it to health
// fraction
if achieved_fraction.is_none() {
*achieved_fraction = Some(current_fraction)
}
if let Some(achieved_fraction) = achieved_fraction {
// Percentage change that should be applied to max_health // Percentage change that should be applied to max_health
let health_tick = match kind { let potential_fraction = 1.0
+ match kind {
ModifierKind::Additive => { ModifierKind::Additive => {
// `rate * dt` is amount of health, dividing by base max // `rate * dt` is amount of health, dividing by base max
// creates fraction // creates fraction
*rate * dt / health.base_max() potential_amount / health.base_max()
}, },
ModifierKind::Fractional => { ModifierKind::Fractional => {
// `rate * dt` is the fraction // `rate * dt` is the fraction
*rate * dt potential_amount
}, },
}; };
let potential_fraction = *achieved_fraction + health_tick;
// Potential progress towards target fraction, if // Potential progress towards target fraction, if
// target_fraction ~ 1.0 then set progress to 1.0 to avoid // target_fraction ~ 1.0 then set progress to 1.0 to avoid
// divide by zero // divide by zero
@ -490,20 +544,18 @@ fn execute_effect(
// Change achieved_fraction depending on what other buffs have // Change achieved_fraction depending on what other buffs have
// occurred // occurred
if progress > 1.0 { let achieved_fraction = if progress > 1.0 {
// If potential fraction already beyond target fraction, // If potential fraction already beyond target fraction,
// simply multiply max_health_modifier by the target // simply multiply max_health_modifier by the target
// fraction, and set achieved fraction to target_fraction // fraction, and set achieved fraction to target_fraction
*achieved_fraction = *target_fraction; *target_fraction
} else { } else {
// Else have not achieved target yet, update // Else have not achieved target yet, use potential_fraction
// achieved_fraction potential_fraction
*achieved_fraction = potential_fraction; };
}
// Apply achieved_fraction to max_health_modifier // Apply achieved_fraction to max_health_modifier
stat.max_health_modifiers.mult_mod *= *achieved_fraction; stat.max_health_modifiers.mult_mod *= achieved_fraction;
}
}, },
BuffEffect::MovementSpeed(speed) => { BuffEffect::MovementSpeed(speed) => {
stat.move_speed_modifier *= *speed; stat.move_speed_modifier *= *speed;
@ -518,35 +570,8 @@ fn execute_effect(
BuffEffect::PoiseReduction(pr) => { BuffEffect::PoiseReduction(pr) => {
stat.poise_reduction = stat.poise_reduction.max(*pr).min(1.0); stat.poise_reduction = stat.poise_reduction.max(*pr).min(1.0);
}, },
BuffEffect::HealReduction { rate } => { BuffEffect::HealReduction(red) => {
stat.heal_multiplier *= 1.0 - *rate; stat.heal_multiplier *= 1.0 - *red;
}, },
}; };
} }
fn tick_buff(id: u64, buff: &mut Buff, dt: f32, mut expire_buff: impl FnMut(u64)) {
// If a buff is recently applied from an aura, do not tick duration
if buff
.cat_ids
.iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromAura(true)))
{
return;
}
if let Some(remaining_delay) = buff.delay {
buff.delay = remaining_delay.checked_sub(Duration::from_secs_f32(dt));
}
if let Some(remaining_time) = &mut buff.time {
if let Some(new_duration) = remaining_time.checked_sub(Duration::from_secs_f32(dt)) {
// The buff still continues.
*remaining_time = new_duration;
} else {
// checked_sub returns None when remaining time
// went below 0, so set to 0
*remaining_time = Duration::default();
// The buff has expired.
// Remove it.
expire_buff(id);
}
}
}

View File

@ -207,6 +207,7 @@ impl<'a> System<'a> for Sys {
&join_struct, &join_struct,
&read_data.lazy_update, &read_data.lazy_update,
&read_data.dt, &read_data.dt,
&read_data.time,
&read_data.msm, &read_data.msm,
&read_data.ability_map, &read_data.ability_map,
); );
@ -226,6 +227,7 @@ impl<'a> System<'a> for Sys {
&join_struct, &join_struct,
&read_data.lazy_update, &read_data.lazy_update,
&read_data.dt, &read_data.dt,
&read_data.time,
&read_data.msm, &read_data.msm,
&read_data.ability_map, &read_data.ability_map,
); );

View File

@ -558,8 +558,7 @@ impl<'a> AgentData<'a> {
Effect::Buff(BuffEffect { kind, data, .. }) Effect::Buff(BuffEffect { kind, data, .. })
if matches!(kind, Regeneration | Saturation | Potion) => if matches!(kind, Regeneration | Saturation | Potion) =>
{ {
value += data.strength value += data.strength * data.duration.map_or(0.0, |d| d.0 as f32);
* data.duration.map_or(0.0, |d| d.as_secs() as f32);
}, },
Effect::Buff(BuffEffect { kind, .. }) Effect::Buff(BuffEffect { kind, .. })
if matches!(kind, PotionSickness) => if matches!(kind, PotionSickness) =>

View File

@ -203,7 +203,7 @@ impl Client {
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_) | ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _) | ServerGeneral::TimeOfDay(_, _, _)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _) | ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_) | ServerGeneral::CreateEntity(_)

View File

@ -42,7 +42,7 @@ use common::{
npc::{self, get_npc_name}, npc::{self, get_npc_name},
outcome::Outcome, outcome::Outcome,
parse_cmd_args, parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
vol::ReadVol, vol::ReadVol,
@ -53,7 +53,7 @@ use common_net::{
sync::WorldSyncExt, sync::WorldSyncExt,
}; };
use common_state::{BuildAreaError, BuildAreas}; use common_state::{BuildAreaError, BuildAreas};
use core::{cmp::Ordering, convert::TryFrom, time::Duration}; use core::{cmp::Ordering, convert::TryFrom};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use humantime::Duration as HumanDuration; use humantime::Duration as HumanDuration;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -1045,6 +1045,7 @@ fn handle_time(
}; };
server.state.mut_resource::<TimeOfDay>().0 = new_time; server.state.mut_resource::<TimeOfDay>().0 = new_time;
let time = server.state.ecs().read_resource::<Time>();
// Update all clients with the new TimeOfDay (without this they would have to // Update all clients with the new TimeOfDay (without this they would have to
// wait for the next 100th tick to receive the update). // wait for the next 100th tick to receive the update).
@ -1056,6 +1057,7 @@ fn handle_time(
client.prepare(ServerGeneral::TimeOfDay( client.prepare(ServerGeneral::TimeOfDay(
TimeOfDay(new_time), TimeOfDay(new_time),
(*calendar).clone(), (*calendar).clone(),
*time,
)) ))
}); });
let _ = client.send_prepared(&msg); let _ = client.send_prepared(&msg);
@ -1462,6 +1464,7 @@ fn handle_spawn_campfire(
_action: &ServerChatCommand, _action: &ServerChatCommand,
) -> CmdResult<()> { ) -> CmdResult<()> {
let pos = position(server, target, "target")?; let pos = position(server, target, "target")?;
let time = server.state.get_time();
server server
.state .state
.create_object(pos, comp::object::Body::CampfireLit) .create_object(pos, comp::object::Body::CampfireLit)
@ -1476,24 +1479,26 @@ fn handle_spawn_campfire(
Aura::new( Aura::new(
AuraKind::Buff { AuraKind::Buff {
kind: BuffKind::CampfireHeal, kind: BuffKind::CampfireHeal,
data: BuffData::new(0.02, Some(Duration::from_secs(1)), None), data: BuffData::new(0.02, Some(Secs(1.0)), None),
category: BuffCategory::Natural, category: BuffCategory::Natural,
source: BuffSource::World, source: BuffSource::World,
}, },
5.0, 5.0,
None, None,
AuraTarget::All, AuraTarget::All,
Time(time),
), ),
Aura::new( Aura::new(
AuraKind::Buff { AuraKind::Buff {
kind: BuffKind::Burning, kind: BuffKind::Burning,
data: BuffData::new(2.0, Some(Duration::from_secs(10)), None), data: BuffData::new(2.0, Some(Secs(10.0)), None),
category: BuffCategory::Natural, category: BuffCategory::Natural,
source: BuffSource::World, source: BuffSource::World,
}, },
0.7, 0.7,
None, None,
AuraTarget::All, AuraTarget::All,
Time(time),
), ),
])) ]))
.build(); .build();
@ -3516,8 +3521,8 @@ fn handle_buff(
) -> CmdResult<()> { ) -> CmdResult<()> {
if let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) { if let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) {
let strength = strength.unwrap_or(0.01); let strength = strength.unwrap_or(0.01);
let duration = Duration::from_secs_f64(duration.unwrap_or(1.0)); let duration = duration.unwrap_or(1.0);
let buffdata = BuffData::new(strength, Some(duration), None); let buffdata = BuffData::new(strength, Some(Secs(duration)), None);
if buff != "all" { if buff != "all" {
cast_buff(&buff, buffdata, server, target) cast_buff(&buff, buffdata, server, target)
} else { } else {
@ -3535,8 +3540,20 @@ fn cast_buff(kind: &str, data: BuffData, server: &mut Server, target: EcsEntity)
if let Some(buffkind) = parse_buffkind(kind) { if let Some(buffkind) = parse_buffkind(kind) {
let ecs = &server.state.ecs(); let ecs = &server.state.ecs();
let mut buffs_all = ecs.write_storage::<comp::Buffs>(); let mut buffs_all = ecs.write_storage::<comp::Buffs>();
let stats = ecs.read_storage::<comp::Stats>();
let time = ecs.read_resource::<Time>();
if let Some(mut buffs) = buffs_all.get_mut(target) { if let Some(mut buffs) = buffs_all.get_mut(target) {
buffs.insert(Buff::new(buffkind, data, vec![], BuffSource::Command)); buffs.insert(
Buff::new(
buffkind,
data,
vec![],
BuffSource::Command,
*time,
stats.get(target),
),
*time,
);
} }
Ok(()) Ok(())
} else { } else {

View File

@ -14,6 +14,7 @@ use common::{
event::{EventBus, UpdateCharacterMetadata}, event::{EventBus, UpdateCharacterMetadata},
lottery::LootSpec, lottery::LootSpec,
outcome::Outcome, outcome::Outcome,
resources::{Secs, Time},
rtsim::RtSimEntity, rtsim::RtSimEntity,
uid::Uid, uid::Uid,
util::Dir, util::Dir,
@ -21,7 +22,6 @@ use common::{
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use specs::{Builder, Entity as EcsEntity, WorldExt}; use specs::{Builder, Entity as EcsEntity, WorldExt};
use std::time::Duration;
use vek::{Rgb, Vec3}; use vek::{Rgb, Vec3};
use super::group_manip::update_map_markers; use super::group_manip::update_map_markers;
@ -270,6 +270,7 @@ pub fn handle_beam(server: &mut Server, properties: beam::Properties, pos: Pos,
} }
pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) { pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
let time = server.state.get_time();
server server
.state .state
.create_object(Pos(pos), comp::object::Body::CampfireLit) .create_object(Pos(pos), comp::object::Body::CampfireLit)
@ -285,24 +286,26 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
Aura::new( Aura::new(
AuraKind::Buff { AuraKind::Buff {
kind: BuffKind::CampfireHeal, kind: BuffKind::CampfireHeal,
data: BuffData::new(0.02, Some(Duration::from_secs(1)), None), data: BuffData::new(0.02, Some(Secs(1.0)), None),
category: BuffCategory::Natural, category: BuffCategory::Natural,
source: BuffSource::World, source: BuffSource::World,
}, },
5.0, 5.0,
None, None,
AuraTarget::All, AuraTarget::All,
Time(time),
), ),
Aura::new( Aura::new(
AuraKind::Buff { AuraKind::Buff {
kind: BuffKind::Burning, kind: BuffKind::Burning,
data: BuffData::new(2.0, Some(Duration::from_secs(10)), None), data: BuffData::new(2.0, Some(Secs(10.0)), None),
category: BuffCategory::Natural, category: BuffCategory::Natural,
source: BuffSource::World, source: BuffSource::World,
}, },
0.7, 0.7,
None, None,
AuraTarget::All, AuraTarget::All,
Time(time),
), ),
])) ]))
.build(); .build();

View File

@ -25,7 +25,7 @@ use common::{
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
outcome::{HealthChangeInfo, Outcome}, outcome::{HealthChangeInfo, Outcome},
resources::Time, resources::{Secs, Time},
rtsim::RtSimEntity, rtsim::RtSimEntity,
states::utils::{AbilityInfo, StageSection}, states::utils::{AbilityInfo, StageSection},
terrain::{Block, BlockKind, TerrainGrid}, terrain::{Block, BlockKind, TerrainGrid},
@ -1091,6 +1091,7 @@ pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::Bu
let ecs = &server.state.ecs(); let ecs = &server.state.ecs();
let mut buffs_all = ecs.write_storage::<comp::Buffs>(); let mut buffs_all = ecs.write_storage::<comp::Buffs>();
let bodies = ecs.read_storage::<Body>(); let bodies = ecs.read_storage::<Body>();
let time = ecs.read_resource::<Time>();
if let Some(mut buffs) = buffs_all.get_mut(entity) { if let Some(mut buffs) = buffs_all.get_mut(entity) {
use buff::BuffChange; use buff::BuffChange;
match buff_change { match buff_change {
@ -1103,7 +1104,7 @@ pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::Bu
.get(entity) .get(entity)
.map_or(true, |h| !h.is_dead) .map_or(true, |h| !h.is_dead)
{ {
buffs.insert(new_buff); buffs.insert(new_buff, *time);
} }
}, },
BuffChange::RemoveById(ids) => { BuffChange::RemoveById(ids) => {
@ -1307,20 +1308,24 @@ pub fn handle_parry_hook(server: &Server, defender: EcsEntity, attacker: Option<
let duration = char_state let duration = char_state
.durations() .durations()
.and_then(|durs| durs.recover) .and_then(|durs| durs.recover)
.map_or(0.5, |dur| dur.as_secs_f32()) .map_or(0.5, |dur| dur.as_secs_f64())
.max(0.5) .max(0.5)
.mul(2.0); .mul(2.0);
let data = buff::BuffData::new(1.0, Some(Duration::from_secs_f32(duration)), None); let data = buff::BuffData::new(1.0, Some(Secs(duration)), None);
let source = if let Some(uid) = ecs.read_storage::<Uid>().get(defender) { let source = if let Some(uid) = ecs.read_storage::<Uid>().get(defender) {
BuffSource::Character { by: *uid } BuffSource::Character { by: *uid }
} else { } else {
BuffSource::World BuffSource::World
}; };
let time = ecs.read_resource::<Time>();
let stats = ecs.read_storage::<comp::Stats>();
let buff = buff::Buff::new( let buff = buff::Buff::new(
BuffKind::Parried, BuffKind::Parried,
data, data,
vec![buff::BuffCategory::Physical], vec![buff::BuffCategory::Physical],
source, source,
*time,
stats.get(attacker),
); );
server_eventbus.emit_now(ServerEvent::Buff { server_eventbus.emit_now(ServerEvent::Buff {
entity: attacker, entity: attacker,

View File

@ -23,7 +23,7 @@ use common::{
effect::Effect, effect::Effect,
link::{Link, LinkHandle}, link::{Link, LinkHandle},
mounting::Mounting, mounting::Mounting,
resources::{Time, TimeOfDay}, resources::{Secs, Time, TimeOfDay},
slowjob::SlowJobPool, slowjob::SlowJobPool,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
LoadoutBuilder, ViewDistances, LoadoutBuilder, ViewDistances,
@ -38,7 +38,7 @@ use specs::{
saveload::MarkerAllocator, Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, saveload::MarkerAllocator, Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder,
Join, WorldExt, Join, WorldExt,
}; };
use std::time::{Duration, Instant}; use std::time::Instant;
use tracing::{trace, warn}; use tracing::{trace, warn};
use vek::*; use vek::*;
@ -222,16 +222,23 @@ impl StateExt for State {
} }
}, },
Effect::Buff(buff) => { Effect::Buff(buff) => {
let time = self.ecs().read_resource::<Time>();
let stats = self.ecs().read_storage::<comp::Stats>();
self.ecs() self.ecs()
.write_storage::<comp::Buffs>() .write_storage::<comp::Buffs>()
.get_mut(entity) .get_mut(entity)
.map(|mut buffs| { .map(|mut buffs| {
buffs.insert(comp::Buff::new( buffs.insert(
comp::Buff::new(
buff.kind, buff.kind,
buff.data, buff.data,
buff.cat_ids, buff.cat_ids,
comp::BuffSource::Item, comp::BuffSource::Item,
)) *time,
stats.get(entity),
),
*time,
)
}); });
}, },
} }
@ -425,19 +432,21 @@ impl StateExt for State {
aura::{Aura, AuraKind, AuraTarget, Auras}, aura::{Aura, AuraKind, AuraTarget, Auras},
buff::{BuffCategory, BuffData, BuffKind, BuffSource}, buff::{BuffCategory, BuffData, BuffKind, BuffSource},
}; };
let time = self.get_time();
self.ecs_mut() self.ecs_mut()
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(Auras::new(vec![Aura::new( .with(Auras::new(vec![Aura::new(
AuraKind::Buff { AuraKind::Buff {
kind: BuffKind::Invulnerability, kind: BuffKind::Invulnerability,
data: BuffData::new(1.0, Some(Duration::from_secs(1)), None), data: BuffData::new(1.0, Some(Secs(1.0)), None),
category: BuffCategory::Natural, category: BuffCategory::Natural,
source: BuffSource::World, source: BuffSource::World,
}, },
range.unwrap_or(100.0), range.unwrap_or(100.0),
None, None,
AuraTarget::All, AuraTarget::All,
Time(time),
)])) )]))
} }

View File

@ -10,7 +10,7 @@ use common::{
event::EventBus, event::EventBus,
outcome::Outcome, outcome::Outcome,
region::{Event as RegionEvent, RegionMap}, region::{Event as RegionEvent, RegionMap},
resources::{PlayerPhysicsSettings, TimeOfDay}, resources::{PlayerPhysicsSettings, Time, TimeOfDay},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
uid::Uid, uid::Uid,
vol::RectVolSize, vol::RectVolSize,
@ -33,6 +33,7 @@ impl<'a> System<'a> for Sys {
Read<'a, PlayerPhysicsSettings>, Read<'a, PlayerPhysicsSettings>,
TrackedStorages<'a>, TrackedStorages<'a>,
ReadExpect<'a, TimeOfDay>, ReadExpect<'a, TimeOfDay>,
ReadExpect<'a, Time>,
ReadExpect<'a, Calendar>, ReadExpect<'a, Calendar>,
ReadExpect<'a, RegionMap>, ReadExpect<'a, RegionMap>,
ReadExpect<'a, UpdateTrackers>, ReadExpect<'a, UpdateTrackers>,
@ -64,6 +65,7 @@ impl<'a> System<'a> for Sys {
player_physics_settings, player_physics_settings,
tracked_storages, tracked_storages,
time_of_day, time_of_day,
time,
calendar, calendar,
region_map, region_map,
trackers, trackers,
@ -416,7 +418,11 @@ impl<'a> System<'a> for Sys {
let mut tod_lazymsg = None; let mut tod_lazymsg = None;
for client in (&clients).join() { for client in (&clients).join() {
let msg = tod_lazymsg.unwrap_or_else(|| { let msg = tod_lazymsg.unwrap_or_else(|| {
client.prepare(ServerGeneral::TimeOfDay(*time_of_day, (*calendar).clone())) client.prepare(ServerGeneral::TimeOfDay(
*time_of_day,
(*calendar).clone(),
*time,
))
}); });
// We don't care much about stream errors here since they could just represent // We don't care much about stream errors here since they could just represent
// network disconnection, which is handled elsewhere. // network disconnection, which is handled elsewhere.

View File

@ -13,6 +13,7 @@ use common::{
cmd::ServerChatCommand, cmd::ServerChatCommand,
comp, comp,
comp::{inventory::item::armor::Friction, Poise, PoiseState}, comp::{inventory::item::armor::Friction, Poise, PoiseState},
resources::Time,
}; };
use core::mem; use core::mem;
use egui::{ use egui::{
@ -535,6 +536,7 @@ fn selected_entity_window(
.join() .join()
.filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _))| e.id() == entity_id) .filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _))| e.id() == entity_id)
{ {
let time = ecs.read_resource::<Time>();
if let Some(pos) = pos { if let Some(pos) = pos {
if let Some(shape_id) = selected_entity_info.debug_shape_id { if let Some(shape_id) = selected_entity_info.debug_shape_id {
egui_actions.actions.push(EguiAction::DebugShape( egui_actions.actions.push(EguiAction::DebugShape(
@ -645,8 +647,8 @@ fn selected_entity_window(
buffs.buffs.iter().for_each(|(_, v)| { buffs.buffs.iter().for_each(|(_, v)| {
ui.label(format!("{:?}", v.kind)); ui.label(format!("{:?}", v.kind));
ui.label( ui.label(
v.time.map_or("-".to_string(), |time| { v.end_time.map_or("-".to_string(), |end| {
format!("{:?}", time) format!("{:?}", end.0 - time.0)
}), }),
); );
ui.label(format!("{:?}", v.source)); ui.label(format!("{:?}", v.source));
@ -672,7 +674,7 @@ fn selected_entity_window(
Buff { kind, .. } => format!("Buff - {:?}", kind) Buff { kind, .. } => format!("Buff - {:?}", kind)
}); });
ui.label(format!("{:1}", v.radius)); ui.label(format!("{:1}", v.radius));
ui.label(v.duration.map_or("-".to_owned(), |x| format!("{:1}s", x.as_secs()))); ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));
ui.label(format!("{:?}", v.target)); ui.label(format!("{:?}", v.target));
ui.end_row(); ui.end_row();
}); });

View File

@ -9,7 +9,10 @@ use crate::{
}; };
use i18n::Localization; use i18n::Localization;
use common::comp::{BuffKind, Buffs, CharacterState, Energy, Health}; use common::{
comp::{BuffKind, Buffs, CharacterState, Energy, Health},
resources::Time,
};
use conrod_core::{ use conrod_core::{
color, color,
image::Id, image::Id,
@ -49,6 +52,7 @@ pub struct BuffsBar<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
health: &'a Health, health: &'a Health,
energy: &'a Energy, energy: &'a Energy,
time: &'a Time,
} }
impl<'a> BuffsBar<'a> { impl<'a> BuffsBar<'a> {
@ -64,6 +68,7 @@ impl<'a> BuffsBar<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
health: &'a Health, health: &'a Health,
energy: &'a Energy, energy: &'a Energy,
time: &'a Time,
) -> Self { ) -> Self {
Self { Self {
imgs, imgs,
@ -78,6 +83,7 @@ impl<'a> BuffsBar<'a> {
global_state, global_state,
health, health,
energy, energy,
time,
} }
} }
} }
@ -219,10 +225,9 @@ impl<'a> Widget for BuffsBar<'a> {
.enumerate() .enumerate()
.for_each(|(i, (((id, timer_id), mult_id), buff))| { .for_each(|(i, (((id, timer_id), mult_id), buff))| {
let max_duration = buff.kind.max_duration(); let max_duration = buff.kind.max_duration();
let current_duration = buff.dur; let current_duration = buff.end_time.map(|end| end - self.time.0);
let duration_percentage = current_duration.map_or(1000.0, |cur| { let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
.map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0)
}) as u32; // Percentage to determine which frame of the timer overlay is displayed }) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = buff.kind.image(self.imgs); let buff_img = buff.kind.image(self.imgs);
let buff_widget = Image::new(buff_img).w_h(40.0, 40.0); let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
@ -236,13 +241,11 @@ impl<'a> Widget for BuffsBar<'a> {
); );
buff_widget buff_widget
.color( .color(if current_duration.map_or(false, |cur| cur < 10.0) {
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col) Some(pulsating_col)
} else { } else {
Some(norm_col) Some(norm_col)
}, })
)
.set(*id, ui); .set(*id, ui);
if buff.multiplicity() > 1 { if buff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast()) Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
@ -260,7 +263,7 @@ impl<'a> Widget for BuffsBar<'a> {
} }
// Create Buff tooltip // Create Buff tooltip
let (title, desc_txt) = buff.kind.title_description(localized_strings); let (title, desc_txt) = buff.kind.title_description(localized_strings);
let remaining_time = buff.get_buff_time(); let remaining_time = buff.get_buff_time(*self.time);
let click_to_remove = let click_to_remove =
format!("<{}>", &localized_strings.get_msg("buff-remove")); format!("<{}>", &localized_strings.get_msg("buff-remove"));
let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove); let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
@ -304,10 +307,9 @@ impl<'a> Widget for BuffsBar<'a> {
.enumerate() .enumerate()
.for_each(|(i, (((id, timer_id), mult_id), debuff))| { .for_each(|(i, (((id, timer_id), mult_id), debuff))| {
let max_duration = debuff.kind.max_duration(); let max_duration = debuff.kind.max_duration();
let current_duration = debuff.dur; let current_duration = debuff.end_time.map(|end| end - self.time.0);
let duration_percentage = current_duration.map_or(1000.0, |cur| { let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
.map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0)
}) as u32; // Percentage to determine which frame of the timer overlay is displayed }) as u32; // Percentage to determine which frame of the timer overlay is displayed
let debuff_img = debuff.kind.image(self.imgs); let debuff_img = debuff.kind.image(self.imgs);
let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0); let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
@ -321,13 +323,11 @@ impl<'a> Widget for BuffsBar<'a> {
); );
debuff_widget debuff_widget
.color( .color(if current_duration.map_or(false, |cur| cur < 10.0) {
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col) Some(pulsating_col)
} else { } else {
Some(norm_col) Some(norm_col)
}, })
)
.set(*id, ui); .set(*id, ui);
if debuff.multiplicity() > 1 { if debuff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast()) Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
@ -345,7 +345,7 @@ impl<'a> Widget for BuffsBar<'a> {
} }
// Create Debuff tooltip // Create Debuff tooltip
let (title, desc_txt) = debuff.kind.title_description(localized_strings); let (title, desc_txt) = debuff.kind.title_description(localized_strings);
let remaining_time = debuff.get_buff_time(); let remaining_time = debuff.get_buff_time(*self.time);
let desc = format!("{}\n\n{}", desc_txt, remaining_time); let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(self.get_duration_image(duration_percentage)) Image::new(self.get_duration_image(duration_percentage))
.w_h(40.0, 40.0) .w_h(40.0, 40.0)
@ -405,11 +405,10 @@ impl<'a> Widget for BuffsBar<'a> {
buff_vec.iter().enumerate().for_each( buff_vec.iter().enumerate().for_each(
|(i, ((((id, timer_id), txt_id), mult_id), buff))| { |(i, ((((id, timer_id), txt_id), mult_id), buff))| {
let max_duration = buff.kind.max_duration(); let max_duration = buff.kind.max_duration();
let current_duration = buff.dur; let current_duration = buff.end_time.map(|end| end - self.time.0);
// Percentage to determine which frame of the timer overlay is displayed // Percentage to determine which frame of the timer overlay is displayed
let duration_percentage = current_duration.map_or(1000.0, |cur| { let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
.map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0)
}) as u32; }) as u32;
let buff_img = buff.kind.image(self.imgs); let buff_img = buff.kind.image(self.imgs);
let buff_widget = Image::new(buff_img).w_h(40.0, 40.0); let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
@ -422,13 +421,11 @@ impl<'a> Widget for BuffsBar<'a> {
0.0 + x as f64 * (42.0), 0.0 + x as f64 * (42.0),
); );
buff_widget buff_widget
.color( .color(if current_duration.map_or(false, |cur| cur < 10.0) {
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col) Some(pulsating_col)
} else { } else {
Some(norm_col) Some(norm_col)
}, })
)
.set(*id, ui); .set(*id, ui);
if buff.multiplicity() > 1 { if buff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast()) Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
@ -446,7 +443,7 @@ impl<'a> Widget for BuffsBar<'a> {
} }
// Create Buff tooltip // Create Buff tooltip
let (title, desc_txt) = buff.kind.title_description(localized_strings); let (title, desc_txt) = buff.kind.title_description(localized_strings);
let remaining_time = buff.get_buff_time(); let remaining_time = buff.get_buff_time(*self.time);
let click_to_remove = let click_to_remove =
format!("<{}>", &localized_strings.get_msg("buff-remove")); format!("<{}>", &localized_strings.get_msg("buff-remove"));
let desc = if buff.is_buff { let desc = if buff.is_buff {

View File

@ -17,6 +17,7 @@ use client::{self, Client};
use common::{ use common::{
combat, combat,
comp::{group::Role, inventory::item::MaterialStatManifest, invite::InviteKind, Stats}, comp::{group::Role, inventory::item::MaterialStatManifest, invite::InviteKind, Stats},
resources::Time,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
@ -83,6 +84,7 @@ pub struct Group<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
msm: &'a MaterialStatManifest, msm: &'a MaterialStatManifest,
time: &'a Time,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -101,6 +103,7 @@ impl<'a> Group<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
msm: &'a MaterialStatManifest, msm: &'a MaterialStatManifest,
time: &'a Time,
) -> Self { ) -> Self {
Self { Self {
show, show,
@ -114,6 +117,7 @@ impl<'a> Group<'a> {
global_state, global_state,
tooltip_manager, tooltip_manager,
msm, msm,
time,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
} }
} }
@ -532,11 +536,9 @@ impl<'a> Widget for Group<'a> {
let max_duration = buff.kind.max_duration(); let max_duration = buff.kind.max_duration();
let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani); let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0); let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
let current_duration = buff.dur; let current_duration = buff.end_time.map(|end| end - self.time.0);
let duration_percentage = current_duration.map_or(1000.0, |cur| { let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration.map_or(1000.0, |max| { max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
cur.as_secs_f32() / max.as_secs_f32() * 1000.0
})
}) as u32; // Percentage to determine which frame of the timer overlay is displayed }) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = buff.kind.image(self.imgs); let buff_img = buff.kind.image(self.imgs);
let buff_widget = Image::new(buff_img).w_h(15.0, 15.0); let buff_widget = Image::new(buff_img).w_h(15.0, 15.0);
@ -551,20 +553,16 @@ impl<'a> Widget for Group<'a> {
}; };
prev_id = Some(id); prev_id = Some(id);
buff_widget buff_widget
.color( .color(if current_duration.map_or(false, |cur| cur < 10.0) {
if current_duration
.map_or(false, |cur| cur.as_secs_f32() < 10.0)
{
Some(pulsating_col) Some(pulsating_col)
} else { } else {
Some(norm_col) Some(norm_col)
}, })
)
.set(id, ui); .set(id, ui);
// Create Buff tooltip // Create Buff tooltip
let (title, desc_txt) = let (title, desc_txt) =
buff.kind.title_description(localized_strings); buff.kind.title_description(localized_strings);
let remaining_time = buff.get_buff_time(); let remaining_time = buff.get_buff_time(*self.time);
let desc = format!("{}\n\n{}", desc_txt, remaining_time); let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(match duration_percentage as u64 { Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8 875..=1000 => self.imgs.nothing, // 8/8

View File

@ -104,6 +104,7 @@ use common::{
link::Is, link::Is,
mounting::Mount, mounting::Mount,
outcome::Outcome, outcome::Outcome,
resources::{Secs, Time},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{SpriteKind, TerrainChunk, UnlockKind}, terrain::{SpriteKind, TerrainChunk, UnlockKind},
trade::{ReducedInventory, TradeAction}, trade::{ReducedInventory, TradeAction},
@ -481,7 +482,7 @@ impl<'a> BuffIconKind<'a> {
} }
} }
pub fn max_duration(&self) -> Option<Duration> { pub fn max_duration(&self) -> Option<Secs> {
match self { match self {
Self::Buff { data, .. } => data.duration, Self::Buff { data, .. } => data.duration,
Self::Ability { .. } => None, Self::Ability { .. } => None,
@ -563,7 +564,7 @@ impl<'a> Eq for BuffIconKind<'a> {}
pub struct BuffIcon<'a> { pub struct BuffIcon<'a> {
kind: BuffIconKind<'a>, kind: BuffIconKind<'a>,
is_buff: bool, is_buff: bool,
dur: Option<Duration>, end_time: Option<f64>,
} }
impl<'a> BuffIcon<'a> { impl<'a> BuffIcon<'a> {
@ -574,9 +575,9 @@ impl<'a> BuffIcon<'a> {
} }
} }
pub fn get_buff_time(&self) -> String { pub fn get_buff_time(&self, time: Time) -> String {
if let Some(dur) = self.dur { if let Some(end) = self.end_time {
format!("{:.0}s", dur.as_secs_f32()) format!("{:.0}s", end - time.0)
} else { } else {
"".to_string() "".to_string()
} }
@ -600,7 +601,7 @@ impl<'a> BuffIcon<'a> {
Some(BuffIcon { Some(BuffIcon {
kind: BuffIconKind::Ability { ability_id: id }, kind: BuffIconKind::Ability { ability_id: id },
is_buff: true, is_buff: true,
dur: None, end_time: None,
}) })
} else { } else {
None None
@ -619,7 +620,7 @@ impl<'a> BuffIcon<'a> {
multiplicity: count, multiplicity: count,
}, },
is_buff: buff.kind.is_buff(), is_buff: buff.kind.is_buff(),
dur: buff.time, end_time: buff.end_time.map(|end| end.0),
}) })
} }
} }
@ -1491,6 +1492,7 @@ impl Hud {
let alignments = ecs.read_storage::<comp::Alignment>(); let alignments = ecs.read_storage::<comp::Alignment>();
let is_mount = ecs.read_storage::<Is<Mount>>(); let is_mount = ecs.read_storage::<Is<Mount>>();
let char_states = ecs.read_storage::<comp::CharacterState>(); let char_states = ecs.read_storage::<comp::CharacterState>();
let time = ecs.read_resource::<Time>();
// Check if there was a persistence load error of the skillset, and if so // Check if there was a persistence load error of the skillset, and if so
// display a dialog prompt // display a dialog prompt
@ -2335,6 +2337,7 @@ impl Hud {
}, },
_ => Vec::new(), _ => Vec::new(),
}, },
&time,
) )
.x_y(0.0, 100.0) .x_y(0.0, 100.0)
.position_ingame(ingame_pos) .position_ingame(ingame_pos)
@ -2814,6 +2817,7 @@ impl Hud {
let buffs = ecs.read_storage::<comp::Buffs>(); let buffs = ecs.read_storage::<comp::Buffs>();
let char_states = ecs.read_storage::<comp::CharacterState>(); let char_states = ecs.read_storage::<comp::CharacterState>();
let msm = ecs.read_resource::<MaterialStatManifest>(); let msm = ecs.read_resource::<MaterialStatManifest>();
let time = ecs.read_resource::<Time>();
match Buttons::new( match Buttons::new(
&self.imgs, &self.imgs,
@ -2845,6 +2849,7 @@ impl Hud {
global_state, global_state,
tooltip_manager, tooltip_manager,
&msm, &msm,
&time,
) )
.set(self.ids.group_window, ui_widgets) .set(self.ids.group_window, ui_widgets)
{ {
@ -2928,6 +2933,7 @@ impl Hud {
let bodies = ecs.read_storage::<comp::Body>(); let bodies = ecs.read_storage::<comp::Body>();
let poises = ecs.read_storage::<comp::Poise>(); let poises = ecs.read_storage::<comp::Poise>();
let combos = ecs.read_storage::<comp::Combo>(); let combos = ecs.read_storage::<comp::Combo>();
let time = ecs.read_resource::<Time>();
// Combo floater stuffs // Combo floater stuffs
self.floaters.combo_floater = self.floaters.combo_floater.map(|mut f| { self.floaters.combo_floater = self.floaters.combo_floater.map(|mut f| {
f.timer -= dt.as_secs_f64(); f.timer -= dt.as_secs_f64();
@ -3116,6 +3122,7 @@ impl Hud {
global_state, global_state,
health, health,
energy, energy,
&time,
) )
.set(self.ids.buffs, ui_widgets) .set(self.ids.buffs, ui_widgets)
{ {

View File

@ -9,7 +9,10 @@ use crate::{
settings::{ControlSettings, InterfaceSettings}, settings::{ControlSettings, InterfaceSettings},
ui::{fonts::Fonts, Ingameable}, ui::{fonts::Fonts, Ingameable},
}; };
use common::comp::{Buffs, CharacterState, Energy, Health, SpeechBubble, SpeechBubbleType}; use common::{
comp::{Buffs, CharacterState, Energy, Health, SpeechBubble, SpeechBubbleType},
resources::Time,
};
use conrod_core::{ use conrod_core::{
color, color,
position::Align, position::Align,
@ -96,6 +99,7 @@ pub struct Overhead<'a> {
fonts: &'a Fonts, fonts: &'a Fonts,
key_layout: &'a Option<KeyLayout>, key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>, interaction_options: Vec<(GameInput, String)>,
time: &'a Time,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -115,6 +119,7 @@ impl<'a> Overhead<'a> {
fonts: &'a Fonts, fonts: &'a Fonts,
key_layout: &'a Option<KeyLayout>, key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(GameInput, String)>, interaction_options: Vec<(GameInput, String)>,
time: &'a Time,
) -> Self { ) -> Self {
Self { Self {
info, info,
@ -128,6 +133,7 @@ impl<'a> Overhead<'a> {
fonts, fonts,
key_layout, key_layout,
interaction_options, interaction_options,
time,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
} }
} }
@ -264,11 +270,9 @@ impl<'a> Widget for Overhead<'a> {
.for_each(|(i, ((id, timer_id), buff))| { .for_each(|(i, ((id, timer_id), buff))| {
// Limit displayed buffs // Limit displayed buffs
let max_duration = buff.kind.max_duration(); let max_duration = buff.kind.max_duration();
let current_duration = buff.dur; let current_duration = buff.end_time.map(|end| end - self.time.0);
let duration_percentage = current_duration.map_or(1000.0, |cur| { let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration.map_or(1000.0, |max| { max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
cur.as_secs_f32() / max.as_secs_f32() * 1000.0
})
}) as u32; // Percentage to determine which frame of the timer overlay is displayed }) as u32; // Percentage to determine which frame of the timer overlay is displayed
let buff_img = buff.kind.image(self.imgs); let buff_img = buff.kind.image(self.imgs);
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0); let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
@ -281,13 +285,11 @@ impl<'a> Widget for Overhead<'a> {
0.0 + x as f64 * (21.0), 0.0 + x as f64 * (21.0),
); );
buff_widget buff_widget
.color( .color(if current_duration.map_or(false, |cur| cur < 10.0) {
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col) Some(pulsating_col)
} else { } else {
Some(norm_col) Some(norm_col)
}, })
)
.set(id, ui); .set(id, ui);
Image::new(match duration_percentage as u64 { Image::new(match duration_percentage as u64 {

View File

@ -146,7 +146,7 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec<String> {
let mut description = String::new(); let mut description = String::new();
if let Effect::Buff(buff) = effect { if let Effect::Buff(buff) = effect {
let strength = buff.data.strength; let strength = buff.data.strength;
let dur_secs = buff.data.duration.map(|d| d.as_secs_f32()); let dur_secs = buff.data.duration.map(|d| d.0 as f32);
let str_total = dur_secs.map_or(strength, |secs| strength * secs); let str_total = dur_secs.map_or(strength, |secs| strength * secs);
let format_float = let format_float =

View File

@ -16,7 +16,7 @@ use common::{
}, },
figure::Segment, figure::Segment,
outcome::Outcome, outcome::Outcome,
resources::DeltaTime, resources::{DeltaTime, Time},
spiral::Spiral2d, spiral::Spiral2d,
states::{self, utils::StageSection}, states::{self, utils::StageSection},
terrain::{Block, SpriteKind, TerrainChunk, TerrainGrid}, terrain::{Block, SpriteKind, TerrainChunk, TerrainGrid},
@ -1113,9 +1113,13 @@ impl ParticleMgr {
|| { || {
let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100)); let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32); let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
let max_dur = Duration::from_secs(1); let duration = Duration::from_secs_f64(
aura.end_time
.map_or(1.0, |end| end.0 - time)
.clamp(0.0, 1.0),
);
Particle::new_directed( Particle::new_directed(
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)), duration,
time, time,
ParticleMode::EnergyNature, ParticleMode::EnergyNature,
pos, pos,
@ -1145,9 +1149,13 @@ impl ParticleMgr {
|| { || {
let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100)); let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32); let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
let max_dur = Duration::from_secs(1); let duration = Duration::from_secs_f64(
aura.end_time
.map_or(1.0, |end| end.0 - time)
.clamp(0.0, 1.0),
);
Particle::new_directed( Particle::new_directed(
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)), duration,
time, time,
ParticleMode::EnergyHealing, ParticleMode::EnergyHealing,
pos, pos,
@ -1172,9 +1180,13 @@ impl ParticleMgr {
let y = radius * theta.cos(); let y = radius * theta.cos();
Vec2::new(x, y) + pos.xy() Vec2::new(x, y) + pos.xy()
}; };
let max_dur = Duration::from_secs(1); let duration = Duration::from_secs_f64(
aura.end_time
.map_or(1.0, |end| end.0 - time)
.clamp(0.0, 1.0),
);
Particle::new_directed( Particle::new_directed(
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)), duration,
time, time,
ParticleMode::FlameThrower, ParticleMode::FlameThrower,
rand_pos.with_z(pos.z), rand_pos.with_z(pos.z),
@ -1193,9 +1205,13 @@ impl ParticleMgr {
|| { || {
let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100)); let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32); let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
let max_dur = Duration::from_secs(1); let duration = Duration::from_secs_f64(
aura.end_time
.map_or(1.0, |end| end.0 - time)
.clamp(0.0, 1.0),
);
Particle::new_directed( Particle::new_directed(
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)), duration,
time, time,
ParticleMode::EnergyBuffing, ParticleMode::EnergyBuffing,
pos, pos,
@ -1209,9 +1225,8 @@ impl ParticleMgr {
.. ..
} => { } => {
let is_new_aura = aura.data.duration.map_or(true, |max_dur| { let is_new_aura = aura.data.duration.map_or(true, |max_dur| {
aura.duration.map_or(true, |rem_dur| { let rem_dur = aura.end_time.map_or(time, |e| e.0) - time;
rem_dur.as_secs_f32() > max_dur.as_secs_f32() * 0.9 rem_dur > max_dur.0 * 0.9
})
}); });
if is_new_aura { if is_new_aura {
let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5)); let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
@ -1297,11 +1312,11 @@ impl ParticleMgr {
let mut multiplicity = 0; let mut multiplicity = 0;
// Only show particles for potion sickness at the beginning, after the // Only show particles for potion sickness at the beginning, after the
// drinking animation finishes // drinking animation finishes
if buff_ids if buff_ids.0
.iter() .iter()
.filter_map(|id| buffs.buffs.get(id)) .filter_map(|id| buffs.buffs.get(id))
.any(|buff| { .any(|buff| {
matches!(buff.elapsed(), Some(dur) if Duration::from_secs(1) <= dur && dur <= Duration::from_secs_f32(1.5)) matches!(buff.elapsed(Time(time)), dur if (1.0..=1.5).contains(&dur.0))
}) })
{ {
multiplicity = 1; multiplicity = 1;