Implement potion sickness, which causes diminishing returns on healing from potions.

This commit is contained in:
Avi Weinstock 2023-01-13 19:48:59 -05:00
parent da9cd79973
commit 78845a0d73
25 changed files with 185 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
assets/voxygen/element/de_buffs/debuff_potionsickness_0.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Duration>,
pub delay: Option<Duration>,
}
#[cfg(not(target_arch = "wasm32"))]
impl BuffData {
pub fn new(strength: f32, duration: Option<Duration>) -> Self { Self { strength, duration } }
pub fn new(strength: f32, duration: Option<Duration>, delay: Option<Duration>) -> 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<BuffCategory>,
pub time: Option<Duration>,
pub delay: Option<Duration>,
pub effects: Vec<BuffEffect>,
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<Item = &Buff> + '_ {
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<dyn Iterator<Item = &Buff>>
} else {
Box::new(self.buffs.get(&ids[0]).into_iter())
}
})
}
// Gets most powerful buff of a given kind

View File

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

View File

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

View File

@ -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,9 +245,33 @@ 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]) {
let mut buff_kinds = buff_comp
.kinds
.iter()
.map(|(kind, ids)| (*kind, ids.clone()))
.collect::<Vec<(BuffKind, Vec<BuffId>)>>();
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)
@ -273,6 +297,7 @@ impl<'a> System<'a> for Sys {
}
}
}
}
// Remove buffs that expire
if !expired_buffs.is_empty() {
@ -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.

View File

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

View File

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

View File

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

View File

@ -285,7 +285,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
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<f32>) {
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,
},

View File

@ -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::<Uid>().get(defender) {
BuffSource::Character { by: *uid }
} else {

View File

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

View File

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

View File

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

View File

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

View File

@ -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<s
BuffKind::Ensnared { .. } => 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"),
}
}

View File

@ -172,6 +172,11 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec<String> {
"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<String> {
}),
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
})