Merge branch 'potion-sickness' into 'master'

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

See merge request veloren/veloren!3756
This commit is contained in:
Marcel 2023-01-19 22:06:07 +00:00
commit 25c8eb8444
41 changed files with 392 additions and 74 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

1
Cargo.lock generated
View File

@ -6720,6 +6720,7 @@ dependencies = [
"fxhash",
"hashbrown 0.12.3",
"indexmap",
"itertools",
"kiddo 0.1.7",
"lazy_static",
"num-derive",

View File

@ -18,9 +18,10 @@
)),
items: [
(10, "common.items.consumable.potion_big"),
(10, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank3.fullskill"),
],
)
)

View File

@ -18,9 +18,10 @@
)),
items: [
(10, "common.items.consumable.potion_big"),
(10, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank3.fullskill"),
],
)
)

View File

@ -18,9 +18,10 @@
)),
items: [
(10, "common.items.consumable.potion_big"),
(10, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank3.fullskill"),
],
)
)

View File

@ -15,9 +15,10 @@
)),
items: [
(25, "common.items.consumable.potion_big"),
(25, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank3.fullskill"),
],
)
)

View File

@ -18,7 +18,8 @@
)),
items: [
(10, "common.items.consumable.potion_big"),
(10, "common.items.food.sunflower_icetea"),
],
),
meta: [],
)
)

View File

@ -21,7 +21,8 @@
)),
items: [
(10, "common.items.consumable.potion_big"),
(10, "common.items.food.sunflower_icetea"),
],
),
meta: [],
)
)

View File

@ -22,9 +22,10 @@
)),
items: [
(5, "common.items.consumable.potion_minor"),
(5, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank1.fullskill"),
],
)
)

View File

@ -22,9 +22,10 @@
)),
items: [
(25, "common.items.consumable.potion_minor"),
(25, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank2.fullskill"),
],
)
)

View File

@ -33,9 +33,10 @@
)),
items: [
(50, "common.items.consumable.potion_med"),
(50, "common.items.food.sunflower_icetea"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank3.fullskill"),
],
)
)

View File

@ -33,6 +33,7 @@
)),
items: [
(50, "common.items.consumable.potion_big"),
(50, "common.items.food.sunflower_icetea"),
],
),
meta: [

View File

@ -15,6 +15,15 @@ ItemDef(
),
cat_ids: [Natural],
)),
Buff((
kind: PotionSickness,
data: (
strength: 0.33,
duration: Some(( secs: 45, 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: 45, 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: 45, 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: 45, 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

@ -78,6 +78,7 @@ const int BLACK_SMOKE = 37;
const int LIGHTNING = 38;
const int STEAM = 39;
const int BARRELORGAN = 40;
const int POTION_SICKNESS = 41;
// meters per second squared (acceleration)
const float earth_gravity = 9.807;
@ -95,6 +96,12 @@ vec3 linear_motion(vec3 init_offs, vec3 vel) {
return init_offs + vel * lifetime;
}
vec3 quadratic_bezier_motion(vec3 start, vec3 ctrl0, vec3 end) {
float t = lifetime;
float u = 1 - lifetime;
return u*u*start + t*u*ctrl0 + t*t*end;
}
vec3 grav_vel(float grav) {
return vec3(0, 0, -grav * lifetime);
}
@ -637,6 +644,18 @@ void main() {
spin_in_axis(vec3(1,0,0),0)
);
break;
case POTION_SICKNESS:
attr = Attr(
quadratic_bezier_motion(
vec3(0.0),
vec3(inst_dir.xy, 0.0),
inst_dir
),
vec3((2.0 * (1 - slow_start(0.8)))),
vec4(0.075, 0.625, 0, 1),
spin_in_axis(vec3(1,0,0),0)
);
break;
default:
attr = Attr(
linear_motion(

View File

@ -29,6 +29,7 @@ serde = { version = "1.0.110", features = ["derive", "rc"] }
vek = { version = "0.15.8", features = ["serde"] }
chrono = "0.4.22"
chrono-tz = "0.6"
itertools = "0.10"
sha2 = "0.10"
serde_json = "1.0.50"

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

@ -3,6 +3,7 @@ use crate::uid::Uid;
use core::{cmp::Ordering, time::Duration};
#[cfg(not(target_arch = "wasm32"))]
use hashbrown::HashMap;
use itertools::Either;
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use specs::{Component, DerefFlaggedStorage, VecStorage};
@ -91,6 +92,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 +122,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 +144,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 +214,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 +234,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,16 +419,30 @@ 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,
}
}
/// Calculate how much time has elapsed since the buff was applied (if the
/// buff has a finite duration, otherwise insufficient information
/// 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"))]
@ -527,11 +564,18 @@ impl Buffs {
.map(move |id| (*id, &self.buffs[id]))
}
// Iterates through all active buffs (the most powerful buff of each kind)
pub fn iter_active(&self) -> impl Iterator<Item = &Buff> + '_ {
self.kinds
.values()
.filter_map(move |ids| self.buffs.get(&ids[0]))
// 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 = impl Iterator<Item = &Buff>> + '_ {
self.kinds.iter().map(move |(kind, ids)| {
if kind.stacks() {
// Iterate stackable buffs in reverse order to show the timer of the soonest one
// to expire
Either::Left(ids.iter().filter_map(|id| self.buffs.get(id)).rev())
} else {
Either::Right(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,31 +245,50 @@ 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::<Vec<(BuffKind, Vec<BuffId>)>>();
buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
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 +350,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 +362,9 @@ fn execute_effect(
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 {
entity,
change: HealthChange {
@ -471,6 +493,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 +508,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,8 +528,14 @@ impl<'a> AgentData<'a> {
controller: &mut Controller,
relaxed: bool,
) -> bool {
// Wait for potion sickness to wear off if potions are less than 50% effective.
let heal_multiplier = self.stats.map_or(1.0, |s| s.heal_multiplier);
if heal_multiplier < 0.5 {
return false;
}
let healing_value = |item: &Item| {
let mut value = 0.0;
let mut causes_potion_sickness = false;
if let ItemKind::Consumable { kind, effects, .. } = &*item.kind() {
if matches!(kind, ConsumableKind::Drink)
@ -547,11 +553,22 @@ impl<'a> AgentData<'a> {
value += data.strength
* data.duration.map_or(0.0, |d| d.as_secs() as f32);
},
Effect::Buff(BuffEffect { kind, .. })
if matches!(kind, PotionSickness) =>
{
causes_potion_sickness = true;
},
_ => {},
}
}
}
}
// Prefer non-potion sources of healing when under at least one stack of potion
// sickness, or when incurring potion sickness is unnecessary
if causes_potion_sickness && (heal_multiplier < 1.0 || relaxed) {
value *= 0.1;
}
value as i32
};

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

@ -16,6 +16,7 @@ use conrod_core::{
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
widget_ids! {
struct Ids {
align,
@ -28,6 +29,8 @@ widget_ids! {
debuffs[],
debuff_timers[],
buff_txts[],
buff_multiplicities[],
debuff_multiplicities[],
}
}
@ -87,6 +90,9 @@ pub enum Event {
RemoveBuff(BuffKind),
}
const MULTIPLICITY_COLOR: Color = TEXT_COLOR;
const MULTIPLICITY_FONT_SIZE: u32 = 20;
impl<'a> Widget for BuffsBar<'a> {
type Event = Vec<Event>;
type State = State;
@ -181,6 +187,17 @@ impl<'a> Widget for BuffsBar<'a> {
if state.ids.debuff_timers.len() < debuff_count {
state.update(|state| state.ids.debuff_timers.resize(debuff_count, gen));
};
if state.ids.buff_multiplicities.len() < 2 * buff_count {
state.update(|state| state.ids.buff_multiplicities.resize(2 * buff_count, gen));
};
if state.ids.debuff_multiplicities.len() < 2 * debuff_count {
state.update(|state| {
state
.ids
.debuff_multiplicities
.resize(2 * debuff_count, gen)
});
};
// Create Buff Widgets
let mut buff_vec = state
@ -189,16 +206,18 @@ impl<'a> Widget for BuffsBar<'a> {
.iter()
.copied()
.zip(state.ids.buff_timers.iter().copied())
.zip(state.ids.buff_multiplicities.chunks(2))
.zip(buff_icons.iter().filter(|info| info.is_buff))
.collect::<Vec<_>>();
// Sort the buffs by kind
buff_vec.sort_by_key(|((_id, _timer_id), buff)| std::cmp::Reverse(buff.kind));
buff_vec
.sort_by_key(|(((_id, _timer_id), _mult_id), buff)| std::cmp::Reverse(buff.kind));
buff_vec
.iter()
.enumerate()
.for_each(|(i, ((id, timer_id), buff))| {
.for_each(|(i, (((id, timer_id), mult_id), buff))| {
let max_duration = buff.kind.max_duration();
let current_duration = buff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
@ -225,6 +244,20 @@ impl<'a> Widget for BuffsBar<'a> {
},
)
.set(*id, ui);
if buff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
.bottom_right_with_margins_on(*id, 1.0, 1.0)
.wh_of(mult_id[1])
.graphics_for(*id)
.set(mult_id[0], ui);
Text::new(&format!("{}", buff.multiplicity()))
.middle_of(mult_id[0])
.graphics_for(*id)
.font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
.font_id(self.fonts.cyri.conrod_id)
.color(MULTIPLICITY_COLOR)
.set(mult_id[1], ui);
}
// Create Buff tooltip
let (title, desc_txt) = buff.kind.title_description(localized_strings);
let remaining_time = buff.get_buff_time();
@ -259,16 +292,17 @@ impl<'a> Widget for BuffsBar<'a> {
.iter()
.copied()
.zip(state.ids.debuff_timers.iter().copied())
.zip(state.ids.debuff_multiplicities.chunks(2))
.zip(buff_icons.iter().filter(|info| !info.is_buff))
.collect::<Vec<_>>();
// Sort the debuffs by kind
debuff_vec.sort_by_key(|((_id, _timer_id), debuff)| debuff.kind);
debuff_vec.sort_by_key(|(((_id, _timer_id), _mult_id), debuff)| debuff.kind);
debuff_vec
.iter()
.enumerate()
.for_each(|(i, ((id, timer_id), debuff))| {
.for_each(|(i, (((id, timer_id), mult_id), debuff))| {
let max_duration = debuff.kind.max_duration();
let current_duration = debuff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
@ -295,6 +329,20 @@ impl<'a> Widget for BuffsBar<'a> {
},
)
.set(*id, ui);
if debuff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
.bottom_right_with_margins_on(*id, 1.0, 1.0)
.wh_of(mult_id[1])
.graphics_for(*id)
.set(mult_id[0], ui);
Text::new(&format!("{}", debuff.multiplicity()))
.middle_of(mult_id[0])
.graphics_for(*id)
.font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
.font_id(self.fonts.cyri.conrod_id)
.color(MULTIPLICITY_COLOR)
.set(mult_id[1], ui);
}
// Create Debuff tooltip
let (title, desc_txt) = debuff.kind.title_description(localized_strings);
let remaining_time = debuff.get_buff_time();
@ -334,6 +382,9 @@ impl<'a> Widget for BuffsBar<'a> {
if state.ids.buff_txts.len() < buff_count {
state.update(|state| state.ids.buff_txts.resize(buff_count, gen));
};
if state.ids.buff_multiplicities.len() < 2 * buff_count {
state.update(|state| state.ids.buff_multiplicities.resize(2 * buff_count, gen));
};
// Create Buff Widgets
@ -344,16 +395,15 @@ impl<'a> Widget for BuffsBar<'a> {
.copied()
.zip(state.ids.buff_timers.iter().copied())
.zip(state.ids.buff_txts.iter().copied())
.zip(state.ids.buff_multiplicities.chunks(2))
.zip(buff_icons.iter())
.collect::<Vec<_>>();
// Sort the buffs by kind
buff_vec.sort_by_key(|((_id, _timer_id), txt_id)| std::cmp::Reverse(txt_id.kind));
buff_vec
.iter()
.enumerate()
.for_each(|(i, (((id, timer_id), txt_id), buff))| {
buff_vec.iter().enumerate().for_each(
|(i, ((((id, timer_id), txt_id), mult_id), buff))| {
let max_duration = buff.kind.max_duration();
let current_duration = buff.dur;
// Percentage to determine which frame of the timer overlay is displayed
@ -380,6 +430,20 @@ impl<'a> Widget for BuffsBar<'a> {
},
)
.set(*id, ui);
if buff.multiplicity() > 1 {
Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
.bottom_right_with_margins_on(*id, 1.0, 1.0)
.wh_of(mult_id[1])
.graphics_for(*id)
.set(mult_id[0], ui);
Text::new(&format!("{}", buff.multiplicity()))
.middle_of(mult_id[0])
.graphics_for(*id)
.font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
.font_id(self.fonts.cyri.conrod_id)
.color(MULTIPLICITY_COLOR)
.set(mult_id[1], ui);
}
// Create Buff tooltip
let (title, desc_txt) = buff.kind.title_description(localized_strings);
let remaining_time = buff.get_buff_time();
@ -420,7 +484,8 @@ impl<'a> Widget for BuffsBar<'a> {
.graphics_for(*timer_id)
.color(TEXT_COLOR)
.set(*txt_id, ui);
});
},
);
}
event
}

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

@ -452,10 +452,16 @@ impl<W: Positionable> Position for W {
}
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum BuffIconKind<'a> {
Buff { kind: BuffKind, data: BuffData },
Ability { ability_id: &'a str },
Buff {
kind: BuffKind,
data: BuffData,
multiplicity: usize,
},
Ability {
ability_id: &'a str,
},
}
impl<'a> BuffIconKind<'a> {
@ -478,7 +484,11 @@ impl<'a> BuffIconKind<'a> {
localized_strings: &'b Localization,
) -> (Cow<'b, str>, Cow<'b, str>) {
match self {
Self::Buff { kind, data } => (
Self::Buff {
kind,
data,
multiplicity: _,
} => (
get_buff_title(*kind, localized_strings),
get_buff_desc(*kind, *data, localized_strings),
),
@ -540,7 +550,7 @@ impl<'a> PartialEq for BuffIconKind<'a> {
impl<'a> Eq for BuffIconKind<'a> {}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub struct BuffIcon<'a> {
kind: BuffIconKind<'a>,
is_buff: bool,
@ -548,6 +558,13 @@ pub struct BuffIcon<'a> {
}
impl<'a> BuffIcon<'a> {
pub fn multiplicity(&self) -> usize {
match self.kind {
BuffIconKind::Buff { multiplicity, .. } => multiplicity,
BuffIconKind::Ability { .. } => 1,
}
}
pub fn get_buff_time(&self) -> String {
if let Some(dur) = self.dur {
format!("{:.0}s", dur.as_secs_f32())
@ -559,7 +576,7 @@ impl<'a> BuffIcon<'a> {
pub fn icons_vec(buffs: &comp::Buffs, char_state: &comp::CharacterState) -> Vec<Self> {
buffs
.iter_active()
.map(BuffIcon::from_buff)
.filter_map(BuffIcon::from_buffs)
.chain(BuffIcon::from_char_state(char_state).into_iter())
.collect::<Vec<_>>()
}
@ -581,15 +598,20 @@ impl<'a> BuffIcon<'a> {
}
}
fn from_buff(buff: &comp::Buff) -> Self {
Self {
fn from_buffs<'b, I: Iterator<Item = &'b comp::Buff>>(buffs: I) -> Option<Self> {
let (buff, count) = buffs.fold((None, 0), |(strongest, count), buff| {
(strongest.or(Some(buff)), count + 1)
});
let buff = buff?;
Some(Self {
kind: BuffIconKind::Buff {
kind: buff.kind,
data: buff.data,
multiplicity: count,
},
is_buff: buff.kind.is_buff(),
dur: buff.time,
}
})
}
}
@ -4769,6 +4791,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 +4824,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 +4861,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
})

View File

@ -92,6 +92,7 @@ pub enum ParticleMode {
Lightning = 38,
Steam = 39,
BarrelOrgan = 40,
PotionSickness = 41,
}
impl ParticleMode {

View File

@ -1148,17 +1148,18 @@ impl ParticleMgr {
let time = state.get_time();
let mut rng = thread_rng();
for (interp, pos, buffs, body) in (
for (interp, pos, buffs, body, ori) in (
ecs.read_storage::<Interpolated>().maybe(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<comp::Buffs>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Ori>(),
)
.join()
{
let pos = interp.map_or(pos.0, |i| i.pos);
for (buff_kind, _) in buffs.kinds.iter() {
for (buff_kind, buff_ids) in buffs.kinds.iter() {
use buff::BuffKind;
match buff_kind {
BuffKind::Cursed | BuffKind::Burning => {
@ -1180,7 +1181,7 @@ impl ParticleMgr {
Particle::new_directed(
Duration::from_secs(1),
time,
if matches!(buff_kind, buff::BuffKind::Cursed) {
if matches!(buff_kind, BuffKind::Cursed) {
ParticleMode::CultistFlame
} else {
ParticleMode::FlameThrower
@ -1191,6 +1192,46 @@ impl ParticleMgr {
},
);
},
BuffKind::PotionSickness => {
let mut multiplicity = 0;
// Only show particles for potion sickness at the beginning, after the
// drinking animation finishes
if buff_ids
.iter()
.filter_map(|id| buffs.buffs.get(id))
.any(|buff| {
matches!(buff.elapsed(), Some(dur) if Duration::from_secs(1) <= dur && dur <= Duration::from_secs_f32(1.5))
})
{
multiplicity = 1;
}
self.particles.resize_with(
self.particles.len()
+ multiplicity
* usize::from(
self.scheduler.heartbeats(Duration::from_millis(25)),
),
|| {
let start_pos = pos + Vec3::unit_z() * body.eye_height();
let (radius, theta) =
(rng.gen_range(0.0f32..1.0).sqrt(), rng.gen_range(0.0..TAU));
let end_pos = pos
+ *ori.look_dir()
+ Vec3::<f32>::new(
radius * theta.cos(),
radius * theta.sin(),
0.0,
) * 0.25;
Particle::new_directed(
Duration::from_secs(1),
time,
ParticleMode::PotionSickness,
start_pos,
end_pos,
)
},
);
},
BuffKind::Frenzied => {
self.particles.resize_with(
self.particles.len()