diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e83a9719b..1e331cb3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Doors - Debug hitboxes now scale with the `Scale` component +- Potion quaffing no longer makes characters practically immortal. ## [0.14.0] - 2023-01-07 diff --git a/assets/common/items/boss_drops/potions.ron b/assets/common/items/boss_drops/potions.ron index 72000810c1..4e00c902d9 100644 --- a/assets/common/items/boss_drops/potions.ron +++ b/assets/common/items/boss_drops/potions.ron @@ -15,6 +15,15 @@ ItemDef( ), cat_ids: [Natural], )), + Buff(( + kind: PotionSickness, + data: ( + strength: 0.33, + duration: Some(( secs: 60, nanos: 0, )), + delay: Some(( secs: 1, nanos: 0, )) + ), + cat_ids: [Natural], + )), ] ), quality: High, diff --git a/assets/common/items/consumable/potion_big.ron b/assets/common/items/consumable/potion_big.ron index ffed9947be..e5695e0bd2 100644 --- a/assets/common/items/consumable/potion_big.ron +++ b/assets/common/items/consumable/potion_big.ron @@ -15,6 +15,15 @@ ItemDef( ), cat_ids: [Natural], )), + Buff(( + kind: PotionSickness, + data: ( + strength: 0.33, + duration: Some(( secs: 60, nanos: 0, )), + delay: Some(( secs: 1, nanos: 0, )) + ), + cat_ids: [Natural], + )), ] ), quality: Common, diff --git a/assets/common/items/consumable/potion_med.ron b/assets/common/items/consumable/potion_med.ron index cb27b87c45..4884c9f3c0 100644 --- a/assets/common/items/consumable/potion_med.ron +++ b/assets/common/items/consumable/potion_med.ron @@ -15,6 +15,15 @@ ItemDef( ), cat_ids: [Natural], )), + Buff(( + kind: PotionSickness, + data: ( + strength: 0.33, + duration: Some(( secs: 60, nanos: 0, )), + delay: Some(( secs: 1, nanos: 0, )) + ), + cat_ids: [Natural], + )), ] ), quality: Common, diff --git a/assets/common/items/consumable/potion_minor.ron b/assets/common/items/consumable/potion_minor.ron index 86200c29bd..372802efc2 100644 --- a/assets/common/items/consumable/potion_minor.ron +++ b/assets/common/items/consumable/potion_minor.ron @@ -15,6 +15,15 @@ ItemDef( ), cat_ids: [Natural], )), + Buff(( + kind: PotionSickness, + data: ( + strength: 0.33, + duration: Some(( secs: 60, nanos: 0, )), + delay: Some(( secs: 1, nanos: 0, )) + ), + cat_ids: [Natural], + )), ] ), quality: Common, diff --git a/assets/voxygen/element/de_buffs/debuff_potionsickness_0.png b/assets/voxygen/element/de_buffs/debuff_potionsickness_0.png new file mode 100644 index 0000000000..c343dbe25d --- /dev/null +++ b/assets/voxygen/element/de_buffs/debuff_potionsickness_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f74f4b7ec66798a28cacc4ac7ff14a17b40a4e07d0008d9db9dc8a3d9de91d +size 10569 diff --git a/assets/voxygen/i18n/en/buff.ftl b/assets/voxygen/i18n/en/buff.ftl index 16d54192ff..36eb3480e2 100644 --- a/assets/voxygen/i18n/en/buff.ftl +++ b/assets/voxygen/i18n/en/buff.ftl @@ -67,6 +67,12 @@ buff-desc-fortitude = You can withstand staggers. ## Parried buff-title-parried = Parried buff-desc-parried = You were parried and now are slow to recover. +## Potion sickness +buff-title-potionsickness = Potion sickness +buff-desc-potionsickness = Potions heal you less after recently consuming a potion. +buff-stat-potionsickness = + Decreases the amount you heal from + subsequent potions by { $strength }%. ## Util buff-text-over_seconds = over { $dur_secs } seconds buff-text-for_seconds = for { $dur_secs } seconds diff --git a/common/src/cmd.rs b/common/src/cmd.rs index c78a0da918..1f3d4b8852 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -155,6 +155,7 @@ lazy_static! { BuffKind::Hastened => "hastened", BuffKind::Fortitude => "fortitude", BuffKind::Parried => "parried", + BuffKind::PotionSickness => "potion_sickness", }; let mut buff_parser = HashMap::new(); for kind in BuffKind::iter() { diff --git a/common/src/combat.rs b/common/src/combat.rs index 7135fe1f5b..81f504b4b5 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -1039,6 +1039,7 @@ impl CombatBuff { BuffData::new( self.strength.to_strength(damage, strength_modifier), Some(Duration::from_secs_f32(self.dur_secs)), + None, ), Vec::new(), source, diff --git a/common/src/comp/aura.rs b/common/src/comp/aura.rs index 5f873c60f4..8f98b07cdd 100644 --- a/common/src/comp/aura.rs +++ b/common/src/comp/aura.rs @@ -144,6 +144,7 @@ impl AuraBuffConstructor { data: BuffData { strength: self.strength, duration: self.duration.map(Duration::from_secs_f32), + delay: None, }, category: self.category, source: BuffSource::Character { by: *uid }, diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index 1db9f35287..0a444c900c 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -91,6 +91,9 @@ pub enum BuffKind { /// Causes your attack speed to be slower to emulate the recover duration of /// an ability being lengthened. Parried, + /// Results from drinking a potion. + /// Decreases the health gained from subsequent potions. + PotionSickness, } #[cfg(not(target_arch = "wasm32"))] @@ -118,12 +121,21 @@ impl BuffKind { | BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned - | BuffKind::Parried => false, + | BuffKind::Parried + | BuffKind::PotionSickness => false, } } /// Checks if buff should queue pub fn queues(self) -> bool { matches!(self, BuffKind::Saturation) } + + /// Checks if the buff can affect other buff effects applied in the same + /// tick. + pub fn affects_subsequent_buffs(self) -> bool { matches!(self, BuffKind::PotionSickness) } + + /// Checks if multiple instances of the buff should be processed, instead of + /// only the strongest. + pub fn stacks(self) -> bool { matches!(self, BuffKind::PotionSickness) } } // Struct used to store data relevant to a buff @@ -131,11 +143,18 @@ impl BuffKind { pub struct BuffData { pub strength: f32, pub duration: Option, + pub delay: Option, } #[cfg(not(target_arch = "wasm32"))] impl BuffData { - pub fn new(strength: f32, duration: Option) -> Self { Self { strength, duration } } + pub fn new(strength: f32, duration: Option, delay: Option) -> Self { + Self { + strength, + duration, + delay, + } + } } /// De/buff category ID. @@ -194,6 +213,8 @@ pub enum BuffEffect { GroundFriction(f32), /// Reduces poise damage taken after armor is accounted for by this fraction PoiseReduction(f32), + /// Reduces amount healed by consumables + HealReduction { rate: f32 }, } /// Actual de/buff. @@ -212,6 +233,7 @@ pub struct Buff { pub data: BuffData, pub cat_ids: Vec, pub time: Option, + pub delay: Option, pub effects: Vec, pub source: BuffSource, } @@ -396,12 +418,19 @@ impl Buff { data.duration, ), BuffKind::Parried => (vec![BuffEffect::AttackSpeed(0.5)], data.duration), + BuffKind::PotionSickness => ( + vec![BuffEffect::HealReduction { + rate: data.strength, + }], + data.duration, + ), }; Buff { kind, data, cat_ids, time, + delay: data.delay, effects, source, } @@ -527,11 +556,17 @@ impl Buffs { .map(move |id| (*id, &self.buffs[id])) } - // Iterates through all active buffs (the most powerful buff of each kind) + // Iterates through all active buffs (the most powerful buff of each + // non-stacking kind, and all of the stacking ones) pub fn iter_active(&self) -> impl Iterator + '_ { - self.kinds - .values() - .filter_map(move |ids| self.buffs.get(&ids[0])) + self.kinds.iter().flat_map(move |(kind, ids)| { + if kind.stacks() { + Box::new(ids.iter().filter_map(|id| self.buffs.get(id))) + as Box> + } else { + Box::new(self.buffs.get(&ids[0]).into_iter()) + } + }) } // Gets most powerful buff of a given kind diff --git a/common/src/comp/stats.rs b/common/src/comp/stats.rs index 693083e1ea..cf8321f7a5 100644 --- a/common/src/comp/stats.rs +++ b/common/src/comp/stats.rs @@ -50,6 +50,7 @@ pub struct Stats { pub name: String, pub damage_reduction: f32, pub poise_reduction: f32, + pub heal_multiplier: f32, pub max_health_modifiers: StatsModifier, pub move_speed_modifier: f32, pub attack_speed_modifier: f32, @@ -63,6 +64,7 @@ impl Stats { name, damage_reduction: 0.0, poise_reduction: 0.0, + heal_multiplier: 1.0, max_health_modifiers: StatsModifier::default(), move_speed_modifier: 1.0, attack_speed_modifier: 1.0, @@ -79,6 +81,7 @@ impl Stats { pub fn reset_temp_modifiers(&mut self) { self.damage_reduction = 0.0; self.poise_reduction = 0.0; + self.heal_multiplier = 1.0; self.max_health_modifiers = StatsModifier::default(); self.move_speed_modifier = 1.0; self.attack_speed_modifier = 1.0; diff --git a/common/src/states/self_buff.rs b/common/src/states/self_buff.rs index e550472566..afc4760259 100644 --- a/common/src/states/self_buff.rs +++ b/common/src/states/self_buff.rs @@ -65,6 +65,7 @@ impl CharacterBehavior for Data { BuffData { strength: self.static_data.buff_strength, duration: self.static_data.buff_duration, + delay: None, }, Vec::new(), BuffSource::Character { by: *data.uid }, diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index a9f88c1100..5c763b4f59 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -146,7 +146,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Ensnared, - BuffData::new(1.0, Some(Duration::from_secs_f32(1.0))), + BuffData::new(1.0, Some(Duration::from_secs_f32(1.0)), None), Vec::new(), BuffSource::World, )), @@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(1.0, Some(Duration::from_secs_f32(6.0))), + BuffData::new(1.0, Some(Duration::from_secs_f32(6.0)), None), Vec::new(), BuffSource::World, )), @@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Burning, - BuffData::new(20.0, None), + BuffData::new(20.0, None, None), vec![BuffCategory::Natural], BuffSource::World, )), @@ -245,31 +245,56 @@ impl<'a> System<'a> for Sys { // Iterator over the lists of buffs by kind let buff_comp = &mut *buff_comp; - for buff_ids in buff_comp.kinds.values() { - // Get the strongest of this buff kind - if let Some(buff) = buff_comp.buffs.get_mut(&buff_ids[0]) { - // Get buff owner? - let buff_owner = if let BuffSource::Character { by: owner } = buff.source { - Some(owner) - } else { - None - }; + let mut buff_kinds = buff_comp + .kinds + .iter() + .map(|(kind, ids)| (*kind, ids.clone())) + .collect::)>>(); + buff_kinds.sort_by_key(|(kind, _)| { + if kind.affects_subsequent_buffs() { + 0 + } else { + 1 + } + }); + for (buff_kind, buff_ids) in buff_kinds.into_iter() { + let mut active_buff_ids = Vec::new(); + if buff_kind.stacks() { + // Process all the buffs of this kind + active_buff_ids = buff_ids; + } else { + // Only process the strongest of this buff kind + active_buff_ids.push(buff_ids[0]); + } + for buff_id in active_buff_ids.into_iter() { + if let Some(buff) = buff_comp.buffs.get_mut(&buff_id) { + // Skip the effect of buffs whose start delay hasn't expired. + if buff.delay.is_some() { + continue; + } + // Get buff owner? + let buff_owner = if let BuffSource::Character { by: owner } = buff.source { + Some(owner) + } else { + None + }; - // Now, execute the buff, based on it's delta - for effect in &mut buff.effects { - execute_effect( - effect, - buff.kind, - buff.time, - &read_data, - &mut stat, - health, - energy, - entity, - buff_owner, - &mut server_emitter, - dt, - ); + // Now, execute the buff, based on it's delta + for effect in &mut buff.effects { + execute_effect( + effect, + buff.kind, + buff.time, + &read_data, + &mut stat, + health, + energy, + entity, + buff_owner, + &mut server_emitter, + dt, + ); + } } } } @@ -331,7 +356,7 @@ fn execute_effect( } else { (None, None) }; - let amount = match *kind { + let mut amount = match *kind { ModifierKind::Additive => *accumulated, ModifierKind::Fractional => health.maximum() * *accumulated, }; @@ -343,6 +368,9 @@ fn execute_effect( DamageContributor::new(uid, read_data.groups.get(entity).cloned()) }) }); + if amount > 0.0 { + amount *= stat.heal_multiplier; + } server_emitter.emit(ServerEvent::HealthChange { entity, change: HealthChange { @@ -471,6 +499,9 @@ fn execute_effect( BuffEffect::PoiseReduction(pr) => { stat.poise_reduction = stat.poise_reduction.max(*pr).min(1.0); }, + BuffEffect::HealReduction { rate } => { + stat.heal_multiplier *= 1.0 - *rate; + }, }; } @@ -483,6 +514,9 @@ fn tick_buff(id: u64, buff: &mut Buff, dt: f32, mut expire_buff: impl FnMut(u64) { 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. diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 6ed89c6285..2e4d036217 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -528,6 +528,10 @@ impl<'a> AgentData<'a> { controller: &mut Controller, relaxed: bool, ) -> bool { + // Wait for potion sickness to wear off if potions are less than 20% effective. + if self.stats.map_or(1.0, |s| s.heal_multiplier) < 0.2 { + return false; + } let healing_value = |item: &Item| { let mut value = 0.0; diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 7997f95173..ba1b089c2d 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -45,6 +45,7 @@ pub struct AgentData<'a> { pub active_abilities: &'a ActiveAbilities, pub combo: Option<&'a Combo>, pub buffs: Option<&'a Buffs>, + pub stats: Option<&'a Stats>, pub poise: Option<&'a Poise>, pub cached_spatial_grid: &'a common::CachedSpatialGrid, pub msm: &'a MaterialStatManifest, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b9b81645e3..98a81f9e9a 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1477,7 +1477,7 @@ fn handle_spawn_campfire( Aura::new( AuraKind::Buff { kind: BuffKind::CampfireHeal, - data: BuffData::new(0.02, Some(Duration::from_secs(1))), + data: BuffData::new(0.02, Some(Duration::from_secs(1)), None), category: BuffCategory::Natural, source: BuffSource::World, }, @@ -1488,7 +1488,7 @@ fn handle_spawn_campfire( Aura::new( AuraKind::Buff { kind: BuffKind::Burning, - data: BuffData::new(2.0, Some(Duration::from_secs(10))), + data: BuffData::new(2.0, Some(Duration::from_secs(10)), None), category: BuffCategory::Natural, source: BuffSource::World, }, @@ -3520,7 +3520,7 @@ fn handle_apply_buff( if let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) { let strength = strength.unwrap_or(0.01); let duration = Duration::from_secs_f64(duration.unwrap_or(1.0)); - let buffdata = BuffData::new(strength, Some(duration)); + let buffdata = BuffData::new(strength, Some(duration), None); if buff != "all" { cast_buff(&buff, buffdata, server, target) } else { diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 7a6d3a493f..a472d0ab5a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -285,7 +285,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { Aura::new( AuraKind::Buff { kind: BuffKind::CampfireHeal, - data: BuffData::new(0.02, Some(Duration::from_secs(1))), + data: BuffData::new(0.02, Some(Duration::from_secs(1)), None), category: BuffCategory::Natural, source: BuffSource::World, }, @@ -296,7 +296,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { Aura::new( AuraKind::Buff { kind: BuffKind::Burning, - data: BuffData::new(2.0, Some(Duration::from_secs(10))), + data: BuffData::new(2.0, Some(Duration::from_secs(10)), None), category: BuffCategory::Natural, source: BuffSource::World, }, diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 5b9a5f6569..2a0e54be99 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -1306,7 +1306,7 @@ pub fn handle_parry_hook(server: &Server, defender: EcsEntity, attacker: Option< .map_or(0.5, |dur| dur.as_secs_f32()) .max(0.5) .mul(2.0); - let data = buff::BuffData::new(1.0, Some(Duration::from_secs_f32(duration))); + let data = buff::BuffData::new(1.0, Some(Duration::from_secs_f32(duration)), None); let source = if let Some(uid) = ecs.read_storage::().get(defender) { BuffSource::Character { by: *uid } } else { diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index c669c24469..8c83130b18 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -421,7 +421,7 @@ impl StateExt for State { .with(Auras::new(vec![Aura::new( AuraKind::Buff { kind: BuffKind::Invulnerability, - data: BuffData::new(1.0, Some(Duration::from_secs(1))), + data: BuffData::new(1.0, Some(Duration::from_secs(1)), None), category: BuffCategory::Natural, source: BuffSource::World, }, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f2035841cf..b073eec273 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -199,6 +199,7 @@ impl<'a> System<'a> for Sys { active_abilities, combo, buffs: read_data.buffs.get(entity), + stats: read_data.stats.get(entity), cached_spatial_grid: &read_data.cached_spatial_grid, msm: &read_data.msm, poise: read_data.poises.get(entity), diff --git a/voxygen/i18n-helpers/src/lib.rs b/voxygen/i18n-helpers/src/lib.rs index d2a6087064..9caa7a0ca5 100644 --- a/voxygen/i18n-helpers/src/lib.rs +++ b/voxygen/i18n-helpers/src/lib.rs @@ -106,7 +106,11 @@ pub fn localize_chat_message( tracing::error!("Player was killed by a positive buff!"); "hud-outcome-mysterious" }, - BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned | BuffKind::Parried => { + BuffKind::Wet + | BuffKind::Ensnared + | BuffKind::Poisoned + | BuffKind::Parried + | BuffKind::PotionSickness => { tracing::error!("Player was killed by a debuff that doesn't do damage!"); "hud-outcome-mysterious" }, diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 5e1d1aca98..80a02f5168 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -697,6 +697,7 @@ image_ids! { debuff_ensnared_0: "voxygen.element.de_buffs.debuff_ensnared_0", debuff_poisoned_0: "voxygen.element.de_buffs.debuff_poisoned_0", debuff_parried_0: "voxygen.element.de_buffs.debuff_parried_0", + debuff_potionsickness_0: "voxygen.element.de_buffs.debuff_potionsickness_0", // Animation Frames // Buff Frame diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 333e690719..f2c6fbf503 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -4769,6 +4769,7 @@ pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id { BuffKind::Ensnared => imgs.debuff_ensnared_0, BuffKind::Poisoned => imgs.debuff_poisoned_0, BuffKind::Parried => imgs.debuff_parried_0, + BuffKind::PotionSickness => imgs.debuff_potionsickness_0, } } @@ -4801,6 +4802,7 @@ pub fn get_buff_title(buff: BuffKind, localized_strings: &Localization) -> Cow localized_strings.get_msg("buff-title-ensnared"), BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-title-poisoned"), BuffKind::Parried { .. } => localized_strings.get_msg("buff-title-parried"), + BuffKind::PotionSickness { .. } => localized_strings.get_msg("buff-title-potionsickness"), } } @@ -4837,6 +4839,7 @@ pub fn get_buff_desc(buff: BuffKind, data: BuffData, localized_strings: &Localiz BuffKind::Ensnared { .. } => localized_strings.get_msg("buff-desc-ensnared"), BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-desc-poisoned"), BuffKind::Parried { .. } => localized_strings.get_msg("buff-desc-parried"), + BuffKind::PotionSickness { .. } => localized_strings.get_msg("buff-desc-potionsickness"), } } diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index ade7bc0a71..2a4286d33c 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -172,6 +172,11 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec { "strength" => format_float(strength), }) }, + BuffKind::PotionSickness => { + i18n.get_msg_ctx("buff-stat-potionsickness", &i18n::fluent_args! { + "strength" => format_float(strength * 100.0), + }) + }, BuffKind::Invulnerability => i18n.get_msg("buff-stat-invulnerability"), BuffKind::Bleeding | BuffKind::Burning @@ -199,7 +204,8 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec { }), BuffKind::IncreaseMaxEnergy | BuffKind::IncreaseMaxHealth - | BuffKind::Invulnerability => { + | BuffKind::Invulnerability + | BuffKind::PotionSickness => { i18n.get_msg_ctx("buff-text-for_seconds", &i18n::fluent_args! { "dur_secs" => dur_secs })