Merge branch 'BottledByte/buff_system' into 'master'

The Buff system

Closes 

See merge request 
This commit is contained in:
Samuel Keiffer 2020-10-27 17:11:02 +00:00
commit 5ed81cf97a
70 changed files with 2140 additions and 838 deletions

@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Gave the axe a third attack
- A new secondary charged melee attack for the hammer
- Added Dutch translations
- Buff system
### Changed

@ -1,6 +1,6 @@
ItemDef(
name: "Apple Stick",
description: "Restores 20 Health",
description: "Restores 25 Health",
kind: Consumable(
kind: "AppleStick",
effect: Health((

@ -0,0 +1,12 @@
ItemDef(
name: "Rugged Backpack",
description: "Keeps all your stuff together.",
kind: Armor(
(
kind: Back("Backpack0"),
stats: (
protection: Normal(0.0)),
)
),
quality: Moderate,
)

@ -0,0 +1,12 @@
ItemDef(
name: "Rugged Backpack",
description: "Keeps all your stuff together.",
kind: Armor(
(
kind: Back("BackpackBlue0"),
stats: (
protection: Normal(0.0)),
)
),
quality: Moderate,
)

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 1.8 KiB

Binary file not shown.

After

(image error) Size: 228 B

Binary file not shown.

After

(image error) Size: 1.8 KiB

Binary file not shown.

Before

(image error) Size: 605 B

After

(image error) Size: 2.0 KiB

Binary file not shown.

Before

(image error) Size: 605 B

After

(image error) Size: 2.0 KiB

Binary file not shown.

Before

(image error) Size: 90 B

After

(image error) Size: 1.4 KiB

Binary file not shown.

After

(image error) Size: 2.4 KiB

Binary file not shown.

Before

(image error) Size: 193 B

Binary file not shown.

After

(image error) Size: 3.4 KiB

Binary file not shown.

Before

(image error) Size: 190 B

Binary file not shown.

Before

(image error) Size: 610 B

Binary file not shown.

Before

(image error) Size: 3.5 KiB

Binary file not shown.

Before

(image error) Size: 243 B

Binary file not shown.

Before

(image error) Size: 607 B

Binary file not shown.

Before

(image error) Size: 3.5 KiB

Binary file not shown.

Before

(image error) Size: 608 B

Binary file not shown.

Before

(image error) Size: 3.5 KiB

Binary file not shown.

Before

(image error) Size: 111 B

Binary file not shown.

Before

(image error) Size: 170 B

Binary file not shown.

Before

(image error) Size: 133 B

Binary file not shown.

Before

(image error) Size: 159 B

@ -183,7 +183,8 @@ https://account.veloren.net."#,
"hud.chat.pvp_melee_kill_msg": "[{attacker}] defeated [{victim}]",
"hud.chat.pvp_ranged_kill_msg": "[{attacker}] shot [{victim}]",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] blew up [{victim}]",
"hud.chat.pvp_energy_kill_msg": "[{attacker}] used magic to kill [{victim}]",
"hud.chat.pvp_energy_kill_msg": "[{attacker}] killed [{victim}] with magic",
"hud.chat.pvp_buff_kill_msg": "[{attacker}] killed [{victim}]",
"hud.chat.npc_melee_kill_msg": "{attacker} killed [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]",
@ -290,6 +291,8 @@ magically infused items?"#,
"hud.settings.transparency": "Transparency",
"hud.settings.hotbar": "Hotbar",
"hud.settings.toggle_shortcuts": "Toggle Shortcuts",
"hud.settings.buffs_skillbar": "Buffs at Skillbar",
"hud.settings.buffs_mmap": "Buffs at Minimap",
"hud.settings.toggle_bar_experience": "Toggle Experience Bar",
"hud.settings.scrolling_combat_text": "Scrolling Combat Text",
"hud.settings.single_damage_number": "Single Damage Numbers",
@ -342,9 +345,9 @@ magically infused items?"#,
"hud.settings.refresh_rate": "Refresh Rate",
"hud.settings.save_window_size": "Save window size",
"hud.settings.lighting_rendering_mode": "Lighting Rendering Mode",
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A",
"hud.settings.lighting_rendering_mode.blinnphong": "Type B",
"hud.settings.lighting_rendering_mode.lambertian": "Type L",
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A - High ",
"hud.settings.lighting_rendering_mode.blinnphong": "Type B - Medium",
"hud.settings.lighting_rendering_mode.lambertian": "Type L - Cheap",
"hud.settings.shadow_rendering_mode": "Shadow Rendering Mode",
"hud.settings.shadow_rendering_mode.none": "None",
"hud.settings.shadow_rendering_mode.cheap": "Cheap",
@ -508,6 +511,16 @@ Protection
"esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section
/// Buffs and Debuffs
"buff.remove": "Click to remove",
"buff.title.missing": "Missing Title",
"buff.desc.missing": "Missing Description",
// Buffs
"buff.title.heal_test": "Heal Test",
"buff.desc.heal_test": "This is a test buff to test healing.",
// Debuffs
"debuff.title.bleed_test": "Bleed Test",
"debuff.desc.bleed_test": "This is a test debuff to test bleeding.",
},

@ -65,6 +65,7 @@ VoxygenLocalization(
"common.create": "Crear",
"common.okay": "Ok",
"common.accept": "Aceptar",
"common.decline": "Rechazar",
"common.disclaimer": "Cuidado",
"common.cancel": "Cancelar",
"common.none": "Ninguno",
@ -73,6 +74,13 @@ VoxygenLocalization(
"common.you": "Tu",
"common.automatic": "Automatico",
"common.random": "Aleatorio",
// Settings Window title
"common.interface_settings": "Ajustes de Interfaz",
"common.gameplay_settings": "Ajustes de Jugabilidad",
"common.controls_settings": "Ajustes de Controles",
"common.video_settings": "Ajustes de Graficos",
"common.sound_settings": "Ajustes de Sonido",
"common.language_settings": "Ajustes de Idiomas",
// Message when connection to the server is lost
"common.connection_lost": r#"Conexión perdida!
@ -89,9 +97,11 @@ El cliente está actualizado?"#,
"common.weapons.axe": "Hacha",
"common.weapons.sword": "Espada",
"common.weapons.staff": "Báculo",
"common.weapons.staff": "Vara Mágica",
"common.weapons.bow": "Arco",
"common.weapons.hammer": "Martillo",
"common.weapons.sceptre": "Cetro curativo",
"common.rand_appearance": "Nombre y Apariencia Aleatoria",
/// End Common section
@ -141,6 +151,9 @@ https://account.veloren.net."#,
"main.login.invalid_character": "El personaje seleccionado no es válido",
"main.login.client_crashed": "El cliente crasheó",
"main.login.not_on_whitelist": "No estas en la lista. Contacta al Dueño del Servidor si quieres unirte.",
"main.login.banned": "Usted ha sido baneado por la siguiente razón",
"main.login.kicked": "Te han echado por la siguiente razón",
/// End Main screen section
@ -153,6 +166,7 @@ https://account.veloren.net."#,
"hud.waypoint_saved": "Marcador Guardado",
"hud.press_key_to_show_keybindings_fmt": "Presiona {key} para mostrar los controles del teclado",
"hud.press_key_to_toggle_lantern_fmt": "[{key}] Encender Linterna",
"hud.press_key_to_show_debug_info_fmt": "Presiona {key} para mostrar información de depuración",
"hud.press_key_to_toggle_keybindings_fmt": "Presiona {key} para alternar los controles del teclado",
"hud.press_key_to_toggle_debug_info_fmt": "Presiona {key} para alternar la información de depuración",
@ -160,6 +174,21 @@ https://account.veloren.net."#,
// Chat outputs
"hud.chat.online_msg": "[{name}] se ha conectado.",
"hud.chat.offline_msg": "[{name}] se ha desconectado.",
"hud.chat.default_death_msg": "[{name}] Murió",
"hud.chat.environmental_kill_msg": "[{name}] Murió en {environment}",
"hud.chat.fall_kill_msg": "[{name}] Murió por el daño de la caída",
"hud.chat.suicide_msg": "[{name}] Murió por heridas autoinfligidas",
"hud.chat.pvp_melee_kill_msg": "[{attacker}] Derroto a [{victim}]",
"hud.chat.pvp_ranged_kill_msg": "[{attacker}] Le Disparo a [{victim}]",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] Hizo explotar a [{victim}]",
"hud.chat.pvp_energy_kill_msg": "[{attacker}] usó magia para matar [{victim}]",
"hud.chat.npc_melee_kill_msg": "{attacker} Mató a [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} Le Disparo a [{victim}]",
"hud.chat.npc_explosion_kill_msg": "{attacker} Hizo explotar a [{victim}]",
"hud.chat.loot_msg": "Recogiste [{item}]",
"hud.chat.loot_fail": "Tu inventario está lleno!",
"hud.chat.goodbye": "Adiós!",
@ -197,7 +226,7 @@ Deshazte de ellos haciendo click en ellos y luego arrastralos fuera de la bolsa.
Las noches pueden volverse bastante oscuras en Veloren.
Enciende tu farol escribiendo /lantern en el chat o presionando la G.
Enciende tu Linterna escribiendo /lantern en el chat o presionando la G.
Quieres liberar tu cursor para cerrar esta ventana? Presiona TAB!
@ -232,6 +261,7 @@ objetos infundidos con magia?"#,
"hud.bag.chest": "Torso",
"hud.bag.hands": "Manos",
"hud.bag.lantern": "Linterna",
"hud.bag.glider": "Planeador",
"hud.bag.belt": "Cinturón",
"hud.bag.ring": "Anillo",
"hud.bag.back": "Espalda",
@ -282,8 +312,8 @@ objetos infundidos con magia?"#,
"hud.settings.invert_scroll_zoom": "Invertir Desplazamiento de Zoom",
"hud.settings.invert_mouse_y_axis": "Invertir eje Y del Ratón",
"hud.settings.enable_mouse_smoothing": "Suavizado de la Cámara",
"hud.settings.free_look_behavior": "Comportamiento de vista libre",
"hud.settings.auto_walk_behavior": "Comportamiento al caminar automaticamente",
"hud.settings.free_look_behavior": "Modo de vista libre",
"hud.settings.auto_walk_behavior": "Modo de caminata automática",
"hud.settings.stop_auto_walk_on_input": "Frenar caminata automática",
"hud.settings.view_distance": "Distancia de Visión",
@ -292,22 +322,43 @@ objetos infundidos con magia?"#,
"hud.settings.maximum_fps": "FPS Máximos",
"hud.settings.fov": "Campo de Visión (grados)",
"hud.settings.gamma": "Gama",
"hud.settings.ambiance": "Brillo del Ambiente",
"hud.settings.antialiasing_mode": "Modo Anti-Aliasing",
"hud.settings.cloud_rendering_mode": "Modo de Renderizado de Nubes",
"hud.settings.fluid_rendering_mode": "Modo de Renderizado de Fluidos",
"hud.settings.fluid_rendering_mode.cheap": "Barato",
"hud.settings.fluid_rendering_mode.shiny": "Brillante",
"hud.settings.cloud_rendering_mode.regular": "Regular",
"hud.settings.fluid_rendering_mode": "Modo de Renderizado del Agua",
"hud.settings.fluid_rendering_mode.cheap": "Bajo",
"hud.settings.fluid_rendering_mode.shiny": "Alto",
"hud.settings.cloud_rendering_mode.regular": "Normal",
"hud.settings.fullscreen": "Pantalla Completa",
"hud.settings.save_window_size": "Guardar tamaño de la ventana",
"hud.settings.fullscreen_mode": "Modo de Pantalla Completa",
"hud.settings.fullscreen_mode.exclusive": "Completo",
"hud.settings.fullscreen_mode.borderless": "Con Bordes",
"hud.settings.particles": "Particulas",
"hud.settings.resolution": "Resolución",
"hud.settings.bit_depth": "Profundidad de Bits",
"hud.settings.refresh_rate": "Taza de Refresco",
"hud.settings.save_window_size": " Guardar tamaño de ventana",
"hud.settings.shadow_rendering_mode": "Renderizado de Sombras",
"hud.settings.shadow_rendering_mode.none": "Ninguno",
"hud.settings.shadow_rendering_mode.cheap": "Bajo",
"hud.settings.shadow_rendering_mode.map": "Alto",
"hud.settings.lighting_rendering_mode": "Renderizado de la luz de la Linterna",
"hud.settings.lighting_rendering_mode.ashikhmin": "Tipo A",
"hud.settings.lighting_rendering_mode.blinnphong": "Tipo B",
"hud.settings.lighting_rendering_mode.lambertian": "Tipo L",
"hud.settings.shadow_rendering_mode": "Renderizado de Sombras",
"hud.settings.shadow_rendering_mode.none": "Ninguno",
"hud.settings.shadow_rendering_mode.cheap": "Bajo",
"hud.settings.shadow_rendering_mode.map": "Alto",
"hud.settings.shadow_rendering_mode.map.resolution": "Resolución",
"hud.settings.lod_detail": "Detalle de LoD",
"hud.settings.music_volume": "Volumen de Musica",
"hud.settings.sound_effect_volume": "Volumen de Efectos de Sonido",
"hud.settings.audio_device": "Dispositivo de Audio",
"hud.settings.awaitingkey": "Presiona una tecla...",
"hud.settings.unbound": "Ninguno",
"hud.settings.reset_keybinds": "Reestablecer a los valores predeterminados",
"hud.settings.reset_keybinds": "Reestablecer Controles",
"hud.social": "Lista de jugadores",
"hud.social.online": "En Línea",
@ -315,12 +366,29 @@ objetos infundidos con magia?"#,
"hud.social.not_yet_available": "Aún no esta disponible",
"hud.social.faction": "Facción",
"hud.social.play_online_fmt": "{nb_player} jugador(es) en línea",
"hud.social.name": "Nombre",
"hud.social.level": "Nivel",
"hud.social.zone": "Zona",
"hud.social.account": "Cuenta",
"hud.crafting": "Crafteo",
"hud.crafting.recipes": "Recetas",
"hud.crafting.ingredients": "Ingredientes:",
"hud.crafting.craft": "Fabricar",
"hud.crafting.tool_cata": "Requisitos:",
"hud.group": "Grupo",
"hud.group.invite_to_join": "{name} Te invito a su Grupo!",
"hud.group.invite": "Invitar",
"hud.group.kick": "Echar",
"hud.group.assign_leader": "Asignar Lider",
"hud.group.leave": "Salir del Grupo",
"hud.group.dead" : "Muerto",
"hud.group.out_of_range": "Fuera de Alcance",
"hud.group.add_friend": "Agregar a Amigos",
"hud.group.link_group": "Conectar Grupos",
"hud.group.in_menu": "Eligiendo Personaje",
"hud.group.members": "Miembros del Grupo",
"hud.spell": "Hechizos",
@ -381,6 +449,9 @@ objetos infundidos con magia?"#,
"gameinput.freelook": "Vista Libre",
"gameinput.autowalk": "Caminata Automática",
"gameinput.dance": "Bailar",
"gameinput.select": "Seleccione la Entidad",
"gameinput.acceptgroupinvite": "Aceptar invitación al grupo",
"gameinput.declinegroupinvite": "Rechazar invitación al grupo",
"gameinput.crafting": "Craftear",
"gameinput.sneak": "Agacharse",
"gameinput.swimdown": "Sumergirse",
@ -423,7 +494,7 @@ objetos infundidos con magia?"#,
Estado Físico
Fuerza de Voluntad
Valentía
Protección
"#,

@ -1099,6 +1099,14 @@
"voxel.armor.back.short-2",
(0.0, -2.0, 0.0), (-90.0, 180.0, 0.0), 1.0,
),
Armor(Back("Backpack0")): VoxTrans(
"voxel.armor.back.backpack-0",
(0.0, 0.0, 0.0), (-90.0, 0.0, 0.0), 1.0,
),
Armor(Back("BackpackBlue0")): VoxTrans(
"voxel.armor.back.backpack-0",
(0.0, 0.0, 0.0), (-90.0, 0.0, 0.0), 1.0,
),
// Rings
Armor(Ring("Ring0")): Png(
"element.icons.ring-0",

Binary file not shown.

Binary file not shown.

@ -23,6 +23,14 @@
"Short2": (
vox_spec: ("armor.back.short-2", (-5.0, -1.0, -11.0)),
color: None
),
"Backpack0": (
vox_spec: ("armor.back.backpack-0", (-7.0, -5.0, -10.0)),
color: None
),
"BackpackBlue0": (
vox_spec: ("armor.back.backpack-grey", (-7.0, -5.0, -10.0)),
color: Some((76, 72, 178))
),
},
))

@ -37,6 +37,7 @@ use common::{
terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize},
vol::RectVolSize,
};
use comp::BuffKind;
use futures_executor::block_on;
use futures_timer::Delay;
use futures_util::{select, FutureExt};
@ -644,6 +645,12 @@ impl Client {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
}
pub fn remove_buff(&mut self, buff_id: BuffKind) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
buff_id,
)));
}
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {
@ -977,6 +984,11 @@ impl Client {
// 4) Tick the client's LocalState
self.state.tick(dt, add_foreign_systems, true);
// TODO: avoid emitting these in the first place
self.state
.ecs()
.fetch::<EventBus<common::event::ServerEvent>>()
.recv_all();
// 5) Terrain
let pos = self
@ -1728,6 +1740,11 @@ impl Client {
alias_of_uid(attacker_uid),
alias_of_uid(victim)
),
KillSource::Player(attacker_uid, KillType::Buff) => format!(
"[{}] killed [{}]",
alias_of_uid(attacker_uid),
alias_of_uid(victim)
),
KillSource::NonPlayer(attacker_name, KillType::Melee) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
},
@ -1742,6 +1759,9 @@ impl Client {
attacker_name,
alias_of_uid(victim)
),
KillSource::NonPlayer(attacker_name, KillType::Buff) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
},
KillSource::Environment(environment) => {
format!("[{}] died in {}", alias_of_uid(victim), environment)
},
@ -1767,6 +1787,9 @@ impl Client {
KillSource::Player(attacker_uid, KillType::Energy) => message
.replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)),
KillSource::Player(attacker_uid, KillType::Buff) => message
.replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)),
KillSource::NonPlayer(attacker_name, KillType::Melee) => message
.replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)),
@ -1779,6 +1802,9 @@ impl Client {
KillSource::NonPlayer(attacker_name, KillType::Energy) => message
.replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)),
KillSource::NonPlayer(attacker_name, KillType::Buff) => message
.replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)),
KillSource::Environment(environment) => message
.replace("{name}", &alias_of_uid(victim))
.replace("{environment}", environment),

286
common/src/comp/buff.rs Normal file

@ -0,0 +1,286 @@
use crate::sync::Uid;
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
use std::{cmp::Ordering, collections::HashMap, time::Duration};
/// De/buff Kind.
/// This is used to determine what effects a buff will have
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum BuffKind {
/// Restores health/time for some period
Regeneration,
/// Lowers health over time for some duration
Bleeding,
/// Lower a creature's max health
/// Currently placeholder buff to show other stuff is possible
Cursed,
}
impl BuffKind {
// Checks if buff is buff or debuff
pub fn is_buff(self) -> bool {
match self {
BuffKind::Regeneration { .. } => true,
BuffKind::Bleeding { .. } => false,
BuffKind::Cursed { .. } => false,
}
}
}
// Struct used to store data relevant to a buff
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct BuffData {
pub strength: f32,
pub duration: Option<Duration>,
}
/// De/buff category ID.
/// Similar to `BuffKind`, but to mark a category (for more generic usage, like
/// positive/negative buffs).
#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub enum BuffCategory {
Natural,
Physical,
Magical,
Divine,
PersistOnDeath,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ModifierKind {
Additive,
Multiplicative,
}
/// Data indicating and configuring behaviour of a de/buff.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum BuffEffect {
/// Periodically damages or heals entity
HealthChangeOverTime { rate: f32, accumulated: f32 },
/// Changes maximum health by a certain amount
MaxHealthModifier { value: f32, kind: ModifierKind },
}
/// Actual de/buff.
/// Buff can timeout after some time if `time` is Some. If `time` is None,
/// Buff will last indefinitely, until removed manually (by some action, like
/// uncursing).
///
/// Buff has a kind, which is used to determine the effects in a builder
/// function.
///
/// To provide more classification info when needed,
/// buff can be in one or more buff category.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Buff {
pub kind: BuffKind,
pub data: BuffData,
pub cat_ids: Vec<BuffCategory>,
pub time: Option<Duration>,
pub effects: Vec<BuffEffect>,
pub source: BuffSource,
}
/// Information about whether buff addition or removal was requested.
/// This to implement "on_add" and "on_remove" hooks for constant buffs.
#[derive(Clone, Debug)]
pub enum BuffChange {
/// Adds this buff.
Add(Buff),
/// Removes all buffs with this ID.
RemoveByKind(BuffKind),
/// Removes all buffs with this ID, but not debuffs.
RemoveFromController(BuffKind),
/// Removes buffs of these indices (first vec is for active buffs, second is
/// for inactive buffs), should only be called when buffs expire
RemoveById(Vec<BuffId>),
/// Removes buffs of these categories (first vec is of categories of which
/// all are required, second vec is of categories of which at least one is
/// required, third vec is of categories that will not be removed)
RemoveByCategory {
all_required: Vec<BuffCategory>,
any_required: Vec<BuffCategory>,
none_required: Vec<BuffCategory>,
},
}
impl Buff {
/// Builder function for buffs
pub fn new(
kind: BuffKind,
data: BuffData,
cat_ids: Vec<BuffCategory>,
source: BuffSource,
) -> Self {
let (effects, time) = match kind {
BuffKind::Bleeding => (
vec![BuffEffect::HealthChangeOverTime {
rate: -data.strength,
accumulated: 0.0,
}],
data.duration,
),
BuffKind::Regeneration => (
vec![BuffEffect::HealthChangeOverTime {
rate: data.strength,
accumulated: 0.0,
}],
data.duration,
),
BuffKind::Cursed => (
vec![BuffEffect::MaxHealthModifier {
value: -100. * data.strength,
kind: ModifierKind::Additive,
}],
data.duration,
),
};
Buff {
kind,
data,
cat_ids,
time,
effects,
source,
}
}
}
impl PartialOrd for Buff {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.data.strength > other.data.strength {
Some(Ordering::Greater)
} else if self.data.strength < other.data.strength {
Some(Ordering::Less)
} else if compare_duration(self.time, other.time) {
Some(Ordering::Greater)
} else if compare_duration(other.time, self.time) {
Some(Ordering::Less)
} else {
None
}
}
}
fn compare_duration(a: Option<Duration>, b: Option<Duration>) -> bool {
a.map_or(true, |dur_a| b.map_or(false, |dur_b| dur_a > dur_b))
}
impl PartialEq for Buff {
fn eq(&self, other: &Self) -> bool {
self.data.strength == other.data.strength && self.time == other.time
}
}
/// Source of the de/buff
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub enum BuffSource {
/// Applied by a character
Character { by: Uid },
/// Applied by world, like a poisonous fumes from a swamp
World,
/// Applied by command
Command,
/// Applied by an item
Item,
/// Applied by another buff (like an after-effect)
Buff,
/// Some other source
Unknown,
}
/// Component holding all de/buffs that gets resolved each tick.
/// On each tick, remaining time of buffs get lowered and
/// buff effect of each buff is applied or not, depending on the `BuffEffect`
/// (specs system will decide based on `BuffEffect`, to simplify
/// implementation). TODO: Something like `once` flag for `Buff` to remove the
/// dependence on `BuffEffect` enum?
///
/// In case of one-time buffs, buff effects will be applied on addition
/// and undone on removal of the buff (by the specs system).
/// Example could be decreasing max health, which, if repeated each tick,
/// would be probably an undesired effect).
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Buffs {
/// Uid used for synchronization
id_counter: u64,
/// Maps Kinds of buff to Id's of currently applied buffs of that kind
pub kinds: HashMap<BuffKind, Vec<BuffId>>,
// All currently applied buffs stored by Id
pub buffs: HashMap<BuffId, Buff>,
}
impl Buffs {
fn sort_kind(&mut self, kind: BuffKind) {
if let Some(buff_order) = self.kinds.get_mut(&kind) {
if buff_order.is_empty() {
self.kinds.remove(&kind);
} else {
let buffs = &self.buffs;
// Intentionally sorted in reverse so that the strongest buffs are earlier in
// the vector
buff_order.sort_by(|a, b| buffs[&b].partial_cmp(&buffs[&a]).unwrap());
}
}
}
pub fn remove_kind(&mut self, kind: BuffKind) {
if let Some(buff_ids) = self.kinds.get_mut(&kind) {
for id in buff_ids {
self.buffs.remove(id);
}
self.kinds.remove(&kind);
}
}
pub fn force_insert(&mut self, id: BuffId, buff: Buff) -> BuffId {
let kind = buff.kind;
self.kinds.entry(kind).or_default().push(id);
self.buffs.insert(id, buff);
self.sort_kind(kind);
id
}
pub fn insert(&mut self, buff: Buff) -> BuffId {
self.id_counter += 1;
self.force_insert(self.id_counter, buff)
}
// Iterate through buffs of a given kind in effect order (most powerful first)
pub fn iter_kind(&self, kind: BuffKind) -> impl Iterator<Item = (BuffId, &Buff)> + '_ {
self.kinds
.get(&kind)
.map(|ids| ids.iter())
.unwrap_or_else(|| (&[]).iter())
.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()
.map(move |ids| self.buffs.get(&ids[0]))
.filter(|buff| buff.is_some())
.map(|buff| buff.unwrap())
}
// 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) {
let kind = self.buffs.remove(&buff_id).unwrap().kind;
self.kinds
.get_mut(&kind)
.map(|ids| ids.retain(|id| *id != buff_id));
self.sort_kind(kind);
}
}
pub type BuffId = u64;
impl Component for Buffs {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}

@ -51,6 +51,7 @@ pub enum KillType {
Projectile,
Explosion,
Energy,
Buff,
// Projectile(String), TODO: add projectile name when available
}

@ -1,4 +1,8 @@
use crate::{comp::inventory::slot::Slot, sync::Uid, util::Dir};
use crate::{
comp::{inventory::slot::Slot, BuffKind},
sync::Uid,
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -37,6 +41,7 @@ pub enum ControlEvent {
Unmount,
InventoryManip(InventoryManip),
GroupManip(GroupManip),
RemoveBuff(BuffKind),
Respawn,
}

@ -3,6 +3,7 @@ mod admin;
pub mod agent;
pub mod beam;
pub mod body;
pub mod buff;
mod character_state;
pub mod chat;
mod controller;
@ -31,6 +32,10 @@ pub use body::{
biped_large, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, humanoid, object,
quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, Body, BodyData,
};
pub use buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs,
ModifierKind,
};
pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,

@ -8,6 +8,7 @@ use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
use std::{error::Error, fmt};
/// Specifies what and how much changed current health
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthChange {
pub amount: i32,
@ -20,6 +21,7 @@ pub enum HealthSource {
Projectile { owner: Option<Uid> },
Explosion { owner: Option<Uid> },
Energy { owner: Option<Uid> },
Buff { owner: Option<Uid> },
Suicide,
World,
Revive,
@ -32,6 +34,7 @@ pub enum HealthSource {
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Health {
base_max: u32,
current: u32,
maximum: u32,
pub last_change: (f64, HealthChange),
@ -67,11 +70,21 @@ impl Health {
self.last_change = (0.0, change);
}
// This is private because max hp is based on the level
fn set_maximum(&mut self, amount: u32) {
// This function changes the modified max health value, not the base health
// value. The modified health value takes into account buffs and other temporary
// changes to max health.
pub fn set_maximum(&mut self, amount: u32) {
self.maximum = amount;
self.current = self.current.min(self.maximum);
}
// This is private because max hp is based on the level
fn set_base_max(&mut self, amount: u32) {
self.base_max = amount;
self.current = self.current.min(self.maximum);
}
pub fn reset_max(&mut self) { self.maximum = self.base_max; }
}
#[derive(Debug)]
pub enum StatChangeError {
@ -148,6 +161,8 @@ impl Stats {
// TODO: Delete this once stat points will be a thing
pub fn update_max_hp(&mut self, body: Body) {
self.health
.set_base_max(body.base_health() + body.base_health_increase() * self.level.amount);
self.health
.set_maximum(body.base_health() + body.base_health_increase() * self.level.amount);
}
@ -179,6 +194,7 @@ impl Stats {
health: Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,
@ -198,6 +214,7 @@ impl Stats {
};
stats.update_max_hp(body);
stats
.health
.set_to(stats.health.maximum(), HealthSource::Revive);
@ -213,6 +230,7 @@ impl Stats {
health: Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,

@ -106,6 +106,10 @@ pub enum ServerEvent {
ChatCmd(EcsEntity, String),
/// Send a chat message to the player from an npc or other player
Chat(comp::UnresolvedChatMsg),
Buff {
entity: EcsEntity,
buff_change: comp::BuffChange,
},
}
pub struct EventBus<E> {

@ -13,6 +13,7 @@ sum_type! {
Player(comp::Player),
CanBuild(comp::CanBuild),
Stats(comp::Stats),
Buffs(comp::Buffs),
Energy(comp::Energy),
LightEmitter(comp::LightEmitter),
Item(comp::Item),
@ -42,6 +43,7 @@ sum_type! {
Player(PhantomData<comp::Player>),
CanBuild(PhantomData<comp::CanBuild>),
Stats(PhantomData<comp::Stats>),
Buffs(PhantomData<comp::Buffs>),
Energy(PhantomData<comp::Energy>),
LightEmitter(PhantomData<comp::LightEmitter>),
Item(PhantomData<comp::Item>),
@ -71,6 +73,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Player(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
@ -98,6 +101,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Player(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
@ -125,6 +129,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::Player(_) => sync::handle_remove::<comp::Player>(entity, world),
EcsCompPhantom::CanBuild(_) => sync::handle_remove::<comp::CanBuild>(entity, world),
EcsCompPhantom::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world),
EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world),
EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)

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

@ -601,6 +601,7 @@ impl<'a> System<'a> for Sys {
if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(by) }
| comp::HealthSource::Energy { owner: Some(by) }
| comp::HealthSource::Buff { owner: Some(by) }
| comp::HealthSource::Explosion { owner: Some(by) } =
my_stats.health.last_change.1.cause
{

146
common/src/sys/buff.rs Normal file

@ -0,0 +1,146 @@
use crate::{
comp::{
BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, HealthChange,
HealthSource, Loadout, ModifierKind, Stats,
},
event::{EventBus, ServerEvent},
state::DeltaTime,
sync::Uid,
};
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration;
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
Read<'a, DeltaTime>,
Read<'a, EventBus<ServerEvent>>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Loadout>,
WriteStorage<'a, Stats>,
WriteStorage<'a, Buffs>,
);
fn run(
&mut self,
(entities, dt, server_bus, uids, loadouts, mut stats, mut buffs): Self::SystemData,
) {
let mut server_emitter = server_bus.emitter();
// Set to false to avoid spamming server
buffs.set_event_emission(false);
stats.set_event_emission(false);
for (entity, buff_comp, uid, stat) in (&entities, &mut buffs, &uids, &mut stats).join() {
let mut expired_buffs = Vec::<BuffId>::new();
for (id, buff) in buff_comp.buffs.iter_mut() {
// Tick the buff and subtract delta from it
if let Some(remaining_time) = &mut buff.time {
if let Some(new_duration) =
remaining_time.checked_sub(Duration::from_secs_f32(dt.0))
{
// 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.
expired_buffs.push(*id);
}
}
}
if let Some(loadout) = loadouts.get(entity) {
let damage_reduction = loadout.get_damage_reduction();
if (damage_reduction - 1.0).abs() < f32::EPSILON {
for (id, buff) in buff_comp.buffs.iter() {
if !buff.kind.is_buff() {
expired_buffs.push(*id);
}
}
}
}
// Call to reset stats to base values
stat.health.reset_max();
// Iterator over the lists of buffs by kind
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
};
// Now, execute the buff, based on it's delta
for effect in &mut buff.effects {
match effect {
BuffEffect::HealthChangeOverTime { rate, accumulated } => {
*accumulated += *rate * dt.0;
// Apply damage only once a second (with a minimum of 1 damage), or
// when a buff is removed
if accumulated.abs() > rate.abs().max(10.0)
|| buff.time.map_or(false, |dur| dur == Duration::default())
{
let cause = if *accumulated > 0.0 {
HealthSource::Healing { by: buff_owner }
} else {
HealthSource::Buff { owner: buff_owner }
};
server_emitter.emit(ServerEvent::Damage {
uid: *uid,
change: HealthChange {
amount: *accumulated as i32,
cause,
},
});
*accumulated = 0.0;
};
},
BuffEffect::MaxHealthModifier { value, kind } => match kind {
ModifierKind::Multiplicative => {
stat.health.set_maximum(
(stat.health.maximum() as f32 * *value) as u32,
);
},
ModifierKind::Additive => {
stat.health.set_maximum(
(stat.health.maximum() as f32 + *value) as u32,
);
},
},
};
}
}
}
// Remove buffs that expire
if !expired_buffs.is_empty() {
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::RemoveById(expired_buffs),
});
}
// Remove stats that don't persist on death
if stat.is_dead {
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::RemoveByCategory {
all_required: vec![],
any_required: vec![],
none_required: vec![BuffCategory::PersistOnDeath],
},
});
}
}
// Turned back to true
buffs.set_event_emission(true);
stats.set_event_emission(true);
}
}

@ -1,7 +1,7 @@
use crate::{
comp::{
group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource,
Loadout, Ori, Pos, Scale, Stats,
buff, group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange,
HealthSource, Loadout, Ori, Pos, Scale, Stats,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
@ -9,7 +9,9 @@ use crate::{
sync::Uid,
util::Dir,
};
use rand::{thread_rng, Rng};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*;
pub const BLOCK_EFFICIENCY: f32 = 0.9;
@ -150,6 +152,24 @@ impl<'a> System<'a> for Sys {
cause,
},
});
// Apply bleeding buff on melee hits with 10% chance
// TODO: Don't have buff uniformly applied on all melee attacks
if thread_rng().gen::<f32>() < 0.1 {
use buff::*;
server_emitter.emit(ServerEvent::Buff {
entity: b,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding,
BuffData {
strength: attack.base_damage as f32 / 10.0,
duration: Some(Duration::from_secs(10)),
},
vec![BuffCategory::Physical],
BuffSource::Character { by: *uid },
)),
});
}
attack.hit_count += 1;
}
if attack.knockback != 0.0 && damage.healthchange != 0.0 {

@ -1,7 +1,7 @@
use crate::{
comp::{
slot::{EquipSlot, Slot},
CharacterState, ControlEvent, Controller, InventoryManip,
BuffChange, CharacterState, ControlEvent, Controller, InventoryManip,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
@ -83,6 +83,12 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Mount(entity, mountee_entity));
}
},
ControlEvent::RemoveBuff(buff_id) => {
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::RemoveFromController(buff_id),
});
},
ControlEvent::Unmount => server_emitter.emit(ServerEvent::Unmount(entity)),
ControlEvent::EnableLantern => {
server_emitter.emit(ServerEvent::EnableLantern(entity))

@ -1,5 +1,6 @@
pub mod agent;
mod beam;
mod buff;
pub mod character_behavior;
pub mod combat;
pub mod controller;
@ -23,6 +24,7 @@ pub const PHYS_SYS: &str = "phys_sys";
pub const PROJECTILE_SYS: &str = "projectile_sys";
pub const SHOCKWAVE_SYS: &str = "shockwave_sys";
pub const STATS_SYS: &str = "stats_sys";
pub const BUFFS_SYS: &str = "buffs_sys";
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(agent::Sys, AGENT_SYS, &[]);
@ -32,6 +34,7 @@ pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
CONTROLLER_SYS,
]);
dispatch_builder.add(stats::Sys, STATS_SYS, &[]);
dispatch_builder.add(buff::Sys, BUFFS_SYS, &[]);
dispatch_builder.add(phys::Sys, PHYS_SYS, &[CONTROLLER_SYS, MOUNT_SYS, STATS_SYS]);
dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]);
dispatch_builder.add(shockwave::Sys, SHOCKWAVE_SYS, &[PHYS_SYS]);

@ -6,7 +6,7 @@ use crate::{
use common::{
assets::Asset,
comp::{
self,
self, buff,
chat::{KillSource, KillType},
object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource, Item,
Player, Pos, Stats,
@ -165,11 +165,34 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Energy)
}
},
HealthSource::Buff { owner: Some(by) } => {
// Get energy owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
if state
.ecs()
.read_storage::<Player>()
.get(char_entity)
.is_some()
{
KillSource::Player(by, KillType::Buff)
} else if let Some(stats) =
state.ecs().read_storage::<Stats>().get(char_entity)
{
KillSource::NonPlayer(stats.name.clone(), KillType::Buff)
} else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff)
}
} else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff)
}
},
HealthSource::World => KillSource::FallDamage,
HealthSource::Suicide => KillSource::Suicide,
HealthSource::Projectile { owner: None }
| HealthSource::Explosion { owner: None }
| HealthSource::Energy { owner: None }
| HealthSource::Buff { owner: None }
| HealthSource::Revive
| HealthSource::Command
| HealthSource::LevelUp
@ -189,6 +212,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
let by = if let HealthSource::Attack { by }
| HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) } = cause
{
by
@ -674,3 +698,65 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
PlayerListUpdate::LevelChange(*uid, new_level),
));
}
pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::BuffChange) {
let ecs = &server.state.ecs();
let mut buffs_all = ecs.write_storage::<comp::Buffs>();
if let Some(buffs) = buffs_all.get_mut(entity) {
use buff::BuffChange;
match buff_change {
BuffChange::Add(new_buff) => {
buffs.insert(new_buff);
},
BuffChange::RemoveById(ids) => {
for id in ids {
buffs.remove(id);
}
},
BuffChange::RemoveByKind(kind) => {
buffs.remove_kind(kind);
},
BuffChange::RemoveFromController(kind) => {
if kind.is_buff() {
buffs.remove_kind(kind);
}
},
BuffChange::RemoveByCategory {
all_required,
any_required,
none_required,
} => {
let mut ids_to_remove = Vec::new();
for (id, buff) in buffs.buffs.iter() {
let mut required_met = true;
for required in &all_required {
if !buff.cat_ids.iter().any(|cat| cat == required) {
required_met = false;
break;
}
}
let mut any_met = any_required.is_empty();
for any in &any_required {
if buff.cat_ids.iter().any(|cat| cat == any) {
any_met = true;
break;
}
}
let mut none_met = true;
for none in &none_required {
if buff.cat_ids.iter().any(|cat| cat == none) {
none_met = false;
break;
}
}
if required_met && any_met && none_met {
ids_to_remove.push(*id);
}
}
for id in ids_to_remove {
buffs.remove(id);
}
},
}
}
}

@ -8,8 +8,8 @@ use entity_creation::{
handle_loaded_character_data, handle_shockwave, handle_shoot,
};
use entity_manipulation::{
handle_damage, handle_destroy, handle_explosion, handle_knockback, handle_land_on_ground,
handle_level_up, handle_respawn,
handle_buff, handle_damage, handle_destroy, handle_explosion, handle_knockback,
handle_land_on_ground, handle_level_up, handle_respawn,
};
use group_manip::handle_group;
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
@ -133,6 +133,10 @@ impl Server {
ServerEvent::Chat(msg) => {
chat_messages.push(msg);
},
ServerEvent::Buff {
entity,
buff_change,
} => handle_buff(self, entity, buff_change),
}
}

@ -111,6 +111,7 @@ impl StateExt for State {
.with(comp::Gravity(1.0))
.with(comp::CharacterState::default())
.with(loadout)
.with(comp::Buffs::default())
}
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
@ -202,6 +203,7 @@ impl StateExt for State {
entity,
comp::Alignment::Owned(self.read_component_copied(entity).unwrap()),
);
self.write_component(entity, comp::Buffs::default());
// Make sure physics components are updated
self.write_component(entity, comp::ForceUpdate);

@ -1,7 +1,7 @@
use super::SysTimer;
use common::{
comp::{
BeamSegment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item,
BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item,
LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave,
Stats, Sticky, Vel,
},
@ -44,6 +44,7 @@ pub struct TrackedComps<'a> {
pub body: ReadStorage<'a, Body>,
pub player: ReadStorage<'a, Player>,
pub stats: ReadStorage<'a, Stats>,
pub buffs: ReadStorage<'a, Buffs>,
pub energy: ReadStorage<'a, Energy>,
pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
@ -85,6 +86,10 @@ impl<'a> TrackedComps<'a> {
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.buffs
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.energy
.get(entity)
.cloned()
@ -157,6 +162,7 @@ pub struct ReadTrackers<'a> {
pub body: ReadExpect<'a, UpdateTracker<Body>>,
pub player: ReadExpect<'a, UpdateTracker<Player>>,
pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
@ -187,6 +193,7 @@ impl<'a> ReadTrackers<'a> {
.with_component(&comps.uid, &*self.body, &comps.body, filter)
.with_component(&comps.uid, &*self.player, &comps.player, filter)
.with_component(&comps.uid, &*self.stats, &comps.stats, filter)
.with_component(&comps.uid, &*self.buffs, &comps.buffs, filter)
.with_component(&comps.uid, &*self.energy, &comps.energy, filter)
.with_component(&comps.uid, &*self.can_build, &comps.can_build, filter)
.with_component(
@ -224,6 +231,7 @@ pub struct WriteTrackers<'a> {
body: WriteExpect<'a, UpdateTracker<Body>>,
player: WriteExpect<'a, UpdateTracker<Player>>,
stats: WriteExpect<'a, UpdateTracker<Stats>>,
buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
@ -248,6 +256,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers.body.record_changes(&comps.body);
trackers.player.record_changes(&comps.player);
trackers.stats.record_changes(&comps.stats);
trackers.buffs.record_changes(&comps.buffs);
trackers.energy.record_changes(&comps.energy);
trackers.can_build.record_changes(&comps.can_build);
trackers.light_emitter.record_changes(&comps.light_emitter);
@ -283,6 +292,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
};
log_counts!(uid, "Uids");
log_counts!(body, "Bodies");
log_counts!(buffs, "Buffs");
log_counts!(player, "Players");
log_counts!(stats, "Stats");
log_counts!(energy, "Energies");
@ -307,6 +317,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Body>();
world.register_tracker::<Player>();
world.register_tracker::<Stats>();
world.register_tracker::<Buffs>();
world.register_tracker::<Energy>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();

@ -76,6 +76,7 @@ impl<'a> System<'a> for Sys {
| HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Healing { by: Some(by) } => {
let by_me = my_uid.map_or(false, |&uid| by == uid);
// If the attack was by me also reset this timer

474
voxygen/src/hud/buffs.rs Normal file

@ -0,0 +1,474 @@
use super::{
img_ids::{Imgs, ImgsRot},
BUFF_COLOR, DEBUFF_COLOR, TEXT_COLOR,
};
use crate::{
hud::{get_buff_info, BuffPosition},
i18n::VoxygenLocalization,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
GlobalState,
};
use common::comp::{BuffKind, Buffs};
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
widget_ids! {
struct Ids {
align,
buffs_align,
debuffs_align,
buff_test,
debuff_test,
buffs[],
buff_timers[],
debuffs[],
debuff_timers[],
buff_txts[],
}
}
#[derive(WidgetCommon)]
pub struct BuffsBar<'a> {
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
#[conrod(common_builder)]
common: widget::CommonBuilder,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
buffs: &'a Buffs,
pulse: f32,
global_state: &'a GlobalState,
}
impl<'a> BuffsBar<'a> {
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
pub fn new(
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
buffs: &'a Buffs,
pulse: f32,
global_state: &'a GlobalState,
) -> Self {
Self {
imgs,
fonts,
common: widget::CommonBuilder::default(),
rot_imgs,
tooltip_manager,
localized_strings,
buffs,
pulse,
global_state,
}
}
}
pub struct State {
ids: Ids,
}
pub enum Event {
RemoveBuff(BuffKind),
}
impl<'a> Widget for BuffsBar<'a> {
type Event = Vec<Event>;
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
ids: Ids::new(id_gen),
}
}
fn style(&self) -> Self::Style {}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let mut event = Vec::new();
let localized_strings = self.localized_strings;
let buffs = self.buffs;
let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer
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 buff_position = self.global_state.settings.gameplay.buff_position;
let buffs_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
let edge = &self.rot_imgs.tt_side;
let corner = &self.rot_imgs.tt_corner;
ImageFrame::new(
[edge.cw180, edge.none, edge.cw270, edge.cw90],
[corner.none, corner.cw270, corner.cw90, corner.cw180],
Color::Rgba(0.08, 0.07, 0.04, 1.0),
5.0,
)
})
.title_font_size(self.fonts.cyri.scale(15))
.parent(ui.window)
.desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
if let BuffPosition::Bar = buff_position {
// Alignment
Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT)
.mid_bottom_with_margin_on(ui.window, 92.0)
.set(state.ids.align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_left_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.debuffs_align, ui);
Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
.bottom_right_with_margins_on(state.ids.align, 0.0, 0.0)
.set(state.ids.buffs_align, ui);
// Buffs and Debuffs
let (buff_count, debuff_count) = buffs.iter_active().map(get_buff_info).fold(
(0, 0),
|(buff_count, debuff_count), info| {
if info.is_buff {
(buff_count + 1, debuff_count)
} else {
(buff_count, debuff_count + 1)
}
},
);
// Limit displayed buffs
let buff_count = buff_count.min(22);
let debuff_count = debuff_count.min(22);
let gen = &mut ui.widget_id_generator();
if state.ids.buffs.len() < buff_count {
state.update(|state| state.ids.buffs.resize(buff_count, gen));
};
if state.ids.debuffs.len() < debuff_count {
state.update(|state| state.ids.debuffs.resize(debuff_count, gen));
};
if state.ids.buff_timers.len() < buff_count {
state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
};
if state.ids.debuff_timers.len() < debuff_count {
state.update(|state| state.ids.debuff_timers.resize(debuff_count, gen));
};
// Create Buff Widgets
state
.ids
.buffs
.iter()
.copied()
.zip(state.ids.buff_timers.iter().copied())
.zip(
buffs
.iter_active()
.map(get_buff_info)
.filter(|info| info.is_buff),
)
.enumerate()
.for_each(|(i, ((id, timer_id), buff))| {
let max_duration = buff.data.duration;
let current_duration = buff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration
.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
let buff_img = match buff.kind {
BuffKind::Regeneration { .. } => self.imgs.buff_plus_0,
_ => self.imgs.missing_icon,
};
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 11 slots
let x = i % 11;
let y = i / 11;
let buff_widget = buff_widget.bottom_left_with_margins_on(
state.ids.buffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
buff_widget
.color(
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col)
} else {
Some(norm_col)
},
)
.set(id, ui);
// Create Buff tooltip
let title = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.title.heal_test")
},
_ => localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration.is_none() {
"Permanent".to_string()
} else {
format!("Remaining: {:.0}s", current_duration.unwrap().as_secs_f32())
};
let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove"));
let desc_txt = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.desc.heal_test")
},
_ => localized_strings.get("buff.desc.missing"),
};
let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
// Timer overlay
if Button::image(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, //3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(id)
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
BUFF_COLOR,
)
.set(timer_id, ui)
.was_clicked()
{
event.push(Event::RemoveBuff(buff.kind));
};
});
// Create Debuff Widgets
state
.ids
.debuffs
.iter()
.copied()
.zip(state.ids.debuff_timers.iter().copied())
.zip(
buffs
.iter_active()
.map(get_buff_info)
.filter(|info| !info.is_buff),
)
.enumerate()
.for_each(|(i, ((id, timer_id), debuff))| {
let max_duration = debuff.data.duration;
let current_duration = debuff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration
.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
let debuff_img = match debuff.kind {
BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffKind::Cursed { .. } => self.imgs.debuff_skull_0,
_ => self.imgs.missing_icon,
};
let debuff_widget = Image::new(debuff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 11 slots
let x = i % 11;
let y = i / 11;
let debuff_widget = debuff_widget.bottom_right_with_margins_on(
state.ids.debuffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
debuff_widget
.color(
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col)
} else {
Some(norm_col)
},
)
.set(id, ui);
// Create Debuff tooltip
let title = match debuff.kind {
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.title.bleed_test")
},
_ => localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration.is_none() {
"Permanent".to_string()
} else {
format!("Remaining: {:.0}s", current_duration.unwrap().as_secs_f32())
};
let desc_txt = match debuff.kind {
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.desc.bleed_test")
},
_ => localized_strings.get("debuff.desc.missing"),
};
let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, //3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(id)
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
DEBUFF_COLOR,
)
.set(timer_id, ui);
});
}
if let BuffPosition::Map = buff_position {
// Alignment
Rectangle::fill_with([210.0, 210.0], color::TRANSPARENT)
.top_right_with_margins_on(ui.window, 5.0, 270.0)
.set(state.ids.align, ui);
// Buffs and Debuffs
let buff_count = buffs.kinds.len().min(11);
// Limit displayed buffs
let buff_count = buff_count.min(20);
let gen = &mut ui.widget_id_generator();
if state.ids.buffs.len() < buff_count {
state.update(|state| state.ids.buffs.resize(buff_count, gen));
};
if state.ids.buff_timers.len() < buff_count {
state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
};
if state.ids.buff_txts.len() < buff_count {
state.update(|state| state.ids.buff_txts.resize(buff_count, gen));
};
// Create Buff Widgets
state
.ids
.buffs
.iter()
.copied()
.zip(state.ids.buff_timers.iter().copied())
.zip(state.ids.buff_txts.iter().copied())
.zip(buffs.iter_active().map(get_buff_info))
.enumerate()
.for_each(|(i, (((id, timer_id), txt_id), buff))| {
let max_duration = buff.data.duration;
let current_duration = buff.dur;
// Percentage to determine which frame of the timer overlay is displayed
let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration
.map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0)
}) as u32;
let buff_img = match buff.kind {
BuffKind::Regeneration { .. } => self.imgs.buff_plus_0,
BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffKind::Cursed { .. } => self.imgs.debuff_skull_0,
};
let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
// Sort buffs into rows of 6 slots
let x = i % 6;
let y = i / 6;
let buff_widget = buff_widget.top_right_with_margins_on(
state.ids.align,
0.0 + y as f64 * (54.0),
0.0 + x as f64 * (42.0),
);
buff_widget
.color(
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col)
} else {
Some(norm_col)
},
)
.set(id, ui);
// Create Buff tooltip
let title = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.title.heal_test")
},
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.title.bleed_test")
},
_ => localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration.is_none() {
"".to_string()
} else {
format!("{:.0}s", current_duration.unwrap().as_secs_f32())
};
let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove"));
let desc_txt = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.desc.heal_test")
},
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.desc.bleed_test")
},
_ => localized_strings.get("buff.desc.missing"),
};
let desc = if buff.is_buff {
format!("{}\n\n{}", desc_txt, click_to_remove)
} else {
desc_txt.to_string()
};
// Timer overlay
if Button::image(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, // 3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(40.0, 40.0)
.middle_of(id)
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
if buff.is_buff {
BUFF_COLOR
} else {
DEBUFF_COLOR
},
)
.set(timer_id, ui)
.was_clicked()
{
event.push(Event::RemoveBuff(buff.kind));
}
Text::new(&remaining_time)
.down_from(timer_id, 1.0)
.font_size(self.fonts.cyri.scale(10))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(timer_id)
.color(TEXT_COLOR)
.set(txt_id, ui);
});
}
event
}
}

@ -373,6 +373,10 @@ impl<'a> Widget for Chat<'a> {
.localized_strings
.get("hud.chat.pvp_energy_kill_msg")
.to_string(),
KillSource::Player(_, KillType::Buff) => self
.localized_strings
.get("hud.chat.pvp_buff_kill_msg")
.to_string(),
KillSource::NonPlayer(_, KillType::Melee) => self
.localized_strings
.get("hud.chat.npc_melee_kill_msg")
@ -389,6 +393,10 @@ impl<'a> Widget for Chat<'a> {
.localized_strings
.get("hud.chat.npc_energy_kill_msg")
.to_string(),
KillSource::NonPlayer(_, KillType::Buff) => self
.localized_strings
.get("hud.chat.npc_buff_kill_msg")
.to_string(),
KillSource::Environment(_) => self
.localized_strings
.get("hud.chat.environmental_kill_msg")

@ -1,15 +1,20 @@
use super::{
img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR,
MANA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
img_ids::{Imgs, ImgsRot},
Show, BLACK, BUFF_COLOR, DEBUFF_COLOR, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR,
LOW_HP_COLOR, STAMINA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts,
window::GameInput, GlobalState,
hud::get_buff_info,
i18n::VoxygenLocalization,
settings::Settings,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
window::GameInput,
GlobalState,
};
use client::{self, Client};
use common::{
comp::{group::Role, Stats},
comp::{group::Role, BuffKind, Stats},
sync::{Uid, WorldSyncExt},
};
use conrod_core::{
@ -19,7 +24,6 @@ use conrod_core::{
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
};
use specs::{saveload::MarkerAllocator, WorldExt};
widget_ids! {
pub struct Ids {
group_button,
@ -44,6 +48,8 @@ widget_ids! {
member_panels_txt[],
member_health[],
member_stam[],
buffs[],
buff_timers[],
dead_txt[],
health_txt[],
timeout_bg,
@ -63,10 +69,12 @@ pub struct Group<'a> {
client: &'a Client,
settings: &'a Settings,
imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32,
global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager,
#[conrod(common_builder)]
common: widget::CommonBuilder,
@ -79,20 +87,24 @@ impl<'a> Group<'a> {
client: &'a Client,
settings: &'a Settings,
imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32,
global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager,
) -> Self {
Self {
show,
client,
settings,
imgs,
rot_imgs,
fonts,
localized_strings,
pulse,
global_state,
tooltip_manager,
common: widget::CommonBuilder::default(),
}
}
@ -127,8 +139,26 @@ impl<'a> Widget for Group<'a> {
#[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new();
let localized_strings = self.localized_strings;
let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer
let buffs_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
let edge = &self.rot_imgs.tt_side;
let corner = &self.rot_imgs.tt_corner;
ImageFrame::new(
[edge.cw180, edge.none, edge.cw270, edge.cw90],
[corner.none, corner.cw270, corner.cw90, corner.cw180],
Color::Rgba(0.08, 0.07, 0.04, 1.0),
5.0,
)
})
.title_font_size(self.fonts.cyri.scale(15))
.parent(ui.window)
.desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
// Don't show pets
let group_members = self
@ -293,31 +323,35 @@ impl<'a> Widget for Group<'a> {
let client_state = self.client.state();
let stats = client_state.ecs().read_storage::<common::comp::Stats>();
let energy = client_state.ecs().read_storage::<common::comp::Energy>();
let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
let uid_allocator = client_state
.ecs()
.read_resource::<common::sync::UidAllocator>();
let offset = if self.global_state.settings.gameplay.toggle_debug {
320.0
} else {
110.0
};
// Keep track of the total number of widget ids we are using for buffs
let mut total_buff_count = 0;
for (i, &uid) in group_members.iter().copied().enumerate() {
self.show.group = true;
let entity = uid_allocator.retrieve_entity_internal(uid.into());
let stats = entity.and_then(|entity| stats.get(entity));
let energy = entity.and_then(|entity| energy.get(entity));
let buffs = entity.and_then(|entity| buffs.get(entity));
if let Some(stats) = stats {
let char_name = stats.name.to_string();
let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64;
// change panel positions when debug info is shown
let offset = if self.global_state.settings.gameplay.toggle_debug {
290.0
} else {
110.0
};
let back = if i == 0 {
Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
.down_from(state.ids.member_panels_bg[i - 1], 45.0)
};
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
@ -386,66 +420,174 @@ impl<'a> Widget for Group<'a> {
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&char_name)
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(BLACK)
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt_bg[i], ui);
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(BLACK)
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt_bg[i], ui);
Text::new(&char_name)
.bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt[i], ui);
.bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
.w(300.0) // limit name length display
.set(state.ids.member_panels_txt[i], ui);
if let Some(energy) = energy {
let stam_perc = energy.current() as f64 / energy.maximum() as f64;
// Stamina
Image::new(self.imgs.bar_content)
.w_h(100.0 * stam_perc, 8.0)
.color(Some(MANA_COLOR))
.color(Some(STAMINA_COLOR))
.top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
.set(state.ids.member_stam[i], ui);
}
} else {
// Values N.A.
if let Some(stats) = stats {
if let Some(buffs) = buffs {
// Limit displayed buffs to 11
let buff_count = buffs.kinds.len().min(11);
total_buff_count += buff_count;
let gen = &mut ui.widget_id_generator();
if state.ids.buffs.len() < total_buff_count {
state.update(|state| state.ids.buffs.resize(total_buff_count, gen));
}
if state.ids.buff_timers.len() < total_buff_count {
state.update(|state| {
state.ids.buff_timers.resize(total_buff_count, gen)
});
}
// Create Buff Widgets
let mut prev_id = None;
state
.ids
.buffs
.iter()
.copied()
.zip(state.ids.buff_timers.iter().copied())
.skip(total_buff_count - buff_count)
.zip(buffs.iter_active().map(get_buff_info))
.for_each(|((id, timer_id), buff)| {
let max_duration = buff.data.duration;
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 current_duration = buff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration.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
let buff_img = match buff.kind {
BuffKind::Regeneration { .. } => self.imgs.buff_plus_0,
BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffKind::Cursed { .. } => self.imgs.debuff_skull_0,
};
let buff_widget = Image::new(buff_img).w_h(15.0, 15.0);
let buff_widget = if let Some(id) = prev_id {
buff_widget.right_from(id, 1.0)
} else {
buff_widget.bottom_left_with_margins_on(
state.ids.member_panels_frame[i],
-16.0,
1.0,
)
};
prev_id = Some(id);
buff_widget
.color(
if current_duration
.map_or(false, |cur| cur.as_secs_f32() < 10.0)
{
Some(pulsating_col)
} else {
Some(norm_col)
},
)
.set(id, ui);
// Create Buff tooltip
let title = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.title.heal_test")
},
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.title.bleed_test")
},
_ => localized_strings.get("buff.title.missing"),
};
let remaining_time = if current_duration.is_none() {
"Permanent".to_string()
} else {
format!(
"Remaining: {:.0}s",
current_duration.unwrap().as_secs_f32()
)
};
let desc_txt = match buff.kind {
BuffKind::Regeneration { .. } => {
localized_strings.get("buff.desc.heal_test")
},
BuffKind::Bleeding { .. } => {
localized_strings.get("debuff.desc.bleed_test")
},
_ => localized_strings.get("buff.desc.missing"),
};
let desc = format!("{}\n\n{}", desc_txt, remaining_time);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, // 3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(15.0, 15.0)
.middle_of(id)
.with_tooltip(
self.tooltip_manager,
title,
&desc,
&buffs_tooltip,
if buff.is_buff {
BUFF_COLOR
} else {
DEBUFF_COLOR
},
)
.set(timer_id, ui);
});
} else {
// Values N.A.
Text::new(&stats.name.to_string())
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20)
.font_id(self.fonts.cyri.conrod_id)
.color(GROUP_COLOR)
.set(state.ids.member_panels_txt[i], ui);
};
let offset = if self.global_state.settings.gameplay.toggle_debug {
210.0
} else {
110.0
};
let back = if i == 0 {
Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
};
back.w_h(152.0, 36.0)
.color(Some(TEXT_COLOR))
.set(state.ids.member_panels_bg[i], ui);
// Panel Frame
Image::new(self.imgs.member_frame)
.w_h(152.0, 36.0)
.middle_of(state.ids.member_panels_bg[i])
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&self.localized_strings.get("hud.group.out_of_range"))
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
.font_size(16)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.dead_txt[i], ui);
let back = if i == 0 {
Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0)
} else {
Image::new(self.imgs.member_bg)
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
};
back.w_h(152.0, 36.0)
.color(Some(TEXT_COLOR))
.set(state.ids.member_panels_bg[i], ui);
// Panel Frame
Image::new(self.imgs.member_frame)
.w_h(152.0, 36.0)
.middle_of(state.ids.member_panels_bg[i])
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.member_panels_frame[i], ui);
// Panel Text
Text::new(&self.localized_strings.get("hud.group.out_of_range"))
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
.font_size(16)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.dead_txt[i], ui);
}
}
}

@ -147,22 +147,12 @@ image_ids! {
// Skillbar
level_up: "voxygen.element.misc_bg.level_up",
level_down: "voxygen.element.misc_bg.level_down",
xp_bar_mid: "voxygen.element.skillbar.xp_bar_mid",
xp_bar_left: "voxygen.element.skillbar.xp_bar_left",
xp_bar_right: "voxygen.element.skillbar.xp_bar_right",
healthbar_bg: "voxygen.element.skillbar.healthbar_bg",
energybar_bg: "voxygen.element.skillbar.energybar_bg",
level_down:"voxygen.element.misc_bg.level_down",
bar_content: "voxygen.element.skillbar.bar_content",
skillbar_slot_big: "voxygen.element.skillbar.skillbar_slot_big",
skillbar_slot_big_bg: "voxygen.element.skillbar.skillbar_slot_big",
skillbar_slot_big_act: "voxygen.element.skillbar.skillbar_slot_big",
skillbar_slot: "voxygen.element.skillbar.skillbar_slot",
skillbar_slot_act: "voxygen.element.skillbar.skillbar_slot_active",
skillbar_slot_l: "voxygen.element.skillbar.skillbar_slot_l",
skillbar_slot_r: "voxygen.element.skillbar.skillbar_slot_r",
skillbar_slot_l_act: "voxygen.element.skillbar.skillbar_slot_l_active",
skillbar_slot_r_act: "voxygen.element.skillbar.skillbar_slot_r_active",
skillbar_bg: "voxygen.element.skillbar.bg",
skillbar_frame: "voxygen.element.skillbar.frame",
m1_ico: "voxygen.element.icons.m1",
m2_ico: "voxygen.element.icons.m2",
// Other Icons/Art
skull: "voxygen.element.icons.skull",
@ -282,6 +272,7 @@ image_ids! {
hammerleap: "voxygen.element.icons.skill_hammerleap",
skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash",
skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst",
missing_icon: "voxygen.element.icons.missing_icon_grey",
// Buttons
button: "voxygen.element.buttons.button",
@ -359,6 +350,24 @@ image_ids! {
chat_tell: "voxygen.element.icons.chat.tell",
chat_world: "voxygen.element.icons.chat.world",
// Buffs
buff_plus_0: "voxygen.element.icons.de_buffs.buff_plus_0",
// Debuffs
debuff_skull_0: "voxygen.element.icons.de_buffs.debuff_skull_0",
debuff_bleed_0: "voxygen.element.icons.de_buffs.debuff_bleed_0",
// Animation Frames
// Buff Frame
buff_0: "voxygen.element.animation.buff_frame.1",
buff_1: "voxygen.element.animation.buff_frame.2",
buff_2: "voxygen.element.animation.buff_frame.3",
buff_3: "voxygen.element.animation.buff_frame.4",
buff_4: "voxygen.element.animation.buff_frame.5",
buff_5: "voxygen.element.animation.buff_frame.6",
buff_6: "voxygen.element.animation.buff_frame.7",
buff_7: "voxygen.element.animation.buff_frame.8",
<BlankGraphic>
nothing: (),
}

@ -105,7 +105,7 @@ impl<'a> Widget for MiniMap<'a> {
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
let zoom = state.zoom;
const SCALE: f64 = 1.5;
const SCALE: f64 = 1.5; // TODO Make this a setting
if self.show.mini_map {
Image::new(self.imgs.mmap_frame)
.w_h(174.0 * SCALE, 190.0 * SCALE)

@ -1,4 +1,5 @@
mod bag;
mod buffs;
mod buttons;
mod chat;
mod crafting;
@ -24,6 +25,7 @@ pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState};
pub use settings_window::ScaleChange;
use bag::Bag;
use buffs::BuffsBar;
use buttons::Buttons;
use chat::Chat;
use chrono::NaiveTime;
@ -58,7 +60,10 @@ use client::Client;
use common::{
assets::Asset,
comp,
comp::item::{ItemDesc, Quality},
comp::{
item::{ItemDesc, Quality},
BuffKind,
},
span,
sync::Uid,
terrain::TerrainChunk,
@ -91,10 +96,12 @@ const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0);
const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
const STAMINA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0);
//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
const BUFF_COLOR: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0);
const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
// Item Quality Colors
const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors
@ -239,6 +246,7 @@ widget_ids! {
spell,
skillbar,
buttons,
buffs,
esc_menu,
small_window,
social_window,
@ -264,6 +272,14 @@ widget_ids! {
}
}
#[derive(Clone, Copy)]
pub struct BuffInfo {
kind: comp::BuffKind,
data: comp::BuffData,
is_buff: bool,
dur: Option<Duration>,
}
pub struct DebugInfo {
pub tps: f64,
pub frame_time: Duration,
@ -315,6 +331,7 @@ pub enum Event {
ChatTransp(f32),
ChatCharName(bool),
CrosshairType(CrosshairType),
BuffPosition(BuffPosition),
ToggleXpBar(XpBar),
Intro(Intro),
ToggleBarNumbers(BarNumbers),
@ -348,6 +365,7 @@ pub enum Event {
KickMember(common::sync::Uid),
LeaveGroup,
AssignLeader(common::sync::Uid),
RemoveBuff(BuffKind),
}
// TODO: Are these the possible layouts we want?
@ -388,6 +406,13 @@ pub enum ShortcutNumbers {
On,
Off,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum BuffPosition {
Bar,
Map,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum PressBehavior {
Toggle = 0,
@ -722,6 +747,7 @@ impl Hud {
let ecs = client.state().ecs();
let pos = ecs.read_storage::<comp::Pos>();
let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>();
let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
let uids = ecs.read_storage::<common::sync::Uid>();
@ -1120,11 +1146,12 @@ impl Hud {
let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars
for (pos, info, bubble, stats, height_offset, hpfl, in_group) in (
for (pos, info, bubble, stats, _, height_offset, hpfl, in_group) in (
&entities,
&pos,
interpolated.maybe(),
&stats,
&buffs,
energy.maybe(),
scales.maybe(),
&bodies,
@ -1138,7 +1165,7 @@ impl Hud {
entity != me && !stats.is_dead
})
.filter_map(
|(entity, pos, interpolated, stats, energy, scale, body, hpfl, uid)| {
|(entity, pos, interpolated, stats, buffs, energy, scale, body, hpfl, uid)| {
// Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos);
let in_group = client.group_members().contains_key(uid);
@ -1168,6 +1195,7 @@ impl Hud {
let info = display_overhead_info.then(|| overhead::Info {
name: &stats.name,
stats,
buffs,
energy,
});
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
@ -1182,6 +1210,7 @@ impl Hud {
info,
bubble,
stats,
buffs,
body.height() * scale.map_or(1.0, |s| s.0) + 0.5,
hpfl,
in_group,
@ -1730,6 +1759,7 @@ impl Hud {
// Bag button and nearby icons
let ecs = client.state().ecs();
let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>();
if let Some(player_stats) = stats.get(client.entity()) {
match Buttons::new(
client,
@ -1754,6 +1784,48 @@ impl Hud {
}
}
// Buffs and Debuffs
if let Some(player_buffs) = buffs.get(client.entity()) {
for event in BuffsBar::new(
&self.imgs,
&self.fonts,
&self.rot_imgs,
tooltip_manager,
&self.voxygen_i18n,
&player_buffs,
self.pulse,
&global_state,
)
.set(self.ids.buffs, ui_widgets)
{
match event {
buffs::Event::RemoveBuff(buff_id) => events.push(Event::RemoveBuff(buff_id)),
}
}
}
// Group Window
for event in Group::new(
&mut self.show,
client,
&global_state.settings,
&self.imgs,
&self.rot_imgs,
&self.fonts,
&self.voxygen_i18n,
self.pulse,
&global_state,
tooltip_manager,
)
.set(self.ids.group_window, ui_widgets)
{
match event {
group::Event::Accept => events.push(Event::AcceptInvite),
group::Event::Decline => events.push(Event::DeclineInvite),
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
}
}
// Popup (waypoint saved and similar notifications)
Popup::new(
&self.voxygen_i18n,
@ -1828,8 +1900,8 @@ impl Hud {
Some(stats),
Some(loadout),
Some(energy),
Some(character_state),
Some(controller),
Some(_character_state),
Some(_controller),
Some(inventory),
) = (
stats.get(entity),
@ -1848,9 +1920,9 @@ impl Hud {
&stats,
&loadout,
&energy,
&character_state,
//&character_state,
self.pulse,
&controller,
//&controller,
&inventory,
&self.hotbar,
tooltip_manager,
@ -1996,6 +2068,9 @@ impl Hud {
settings_window::Event::ToggleZoomInvert(zoom_inverted) => {
events.push(Event::ToggleZoomInvert(zoom_inverted));
},
settings_window::Event::BuffPosition(buff_position) => {
events.push(Event::BuffPosition(buff_position));
},
settings_window::Event::ToggleMouseYInvert(mouse_y_inverted) => {
events.push(Event::ToggleMouseYInvert(mouse_y_inverted));
},
@ -2032,9 +2107,6 @@ impl Hud {
settings_window::Event::CrosshairType(crosshair_type) => {
events.push(Event::CrosshairType(crosshair_type));
},
settings_window::Event::ToggleXpBar(xp_bar) => {
events.push(Event::ToggleXpBar(xp_bar));
},
settings_window::Event::ToggleBarNumbers(bar_numbers) => {
events.push(Event::ToggleBarNumbers(bar_numbers));
},
@ -2123,27 +2195,6 @@ impl Hud {
}
}
}
// Group Window
for event in Group::new(
&mut self.show,
client,
&global_state.settings,
&self.imgs,
&self.fonts,
&self.voxygen_i18n,
self.pulse,
&global_state,
)
.set(self.ids.group_window, ui_widgets)
{
match event {
group::Event::Accept => events.push(Event::AcceptInvite),
group::Event::Decline => events.push(Event::DeclineInvite),
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
}
}
// Spellbook
if self.show.spell {
@ -2675,3 +2726,12 @@ pub fn get_quality_col<I: ItemDesc>(item: &I) -> Color {
Quality::Debug => QUALITY_DEBUG,
}
}
// Get info about applied buffs
fn get_buff_info(buff: &comp::Buff) -> BuffInfo {
BuffInfo {
kind: buff.kind,
data: buff.data,
is_buff: buff.kind.is_buff(),
dur: buff.time,
}
}

@ -1,14 +1,16 @@
use super::{
img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR,
MANA_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
};
use crate::{
hud::get_buff_info,
i18n::VoxygenLocalization,
settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable},
};
use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats};
use common::comp::{BuffKind, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats};
use conrod_core::{
color,
position::Align,
widget::{self, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
@ -44,6 +46,11 @@ widget_ids! {
health_txt,
mana_bar,
health_bar_fg,
// Buffs
buffs_align,
buffs[],
buff_timers[],
}
}
@ -51,6 +58,7 @@ widget_ids! {
pub struct Info<'a> {
pub name: &'a str,
pub stats: &'a Stats,
pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>,
}
@ -119,17 +127,25 @@ impl<'a> Ingameable for Overhead<'a> {
// - 1 for HP text
// - If there's mana
// - 1 Rect::new for mana
//
// If there are Buffs
// - 1 Alignment Rectangle
// - 10 + 10 Buffs and Timer Overlays (only if there is no speech bubble)
// If there's a speech bubble
// - 2 Text::new for speech bubble
// - 1 Image::new for icon
// - 10 Image::new for speech bubble (9-slice + tail)
self.info.map_or(0, |info| {
2 + if show_healthbar(info.stats) {
5 + if info.energy.is_some() { 1 } else { 0 }
} else {
0
}
2 + 1
+ if self.bubble.is_none() {
info.buffs.kinds.len().min(10) * 2
} else {
0
}
+ if show_healthbar(info.stats) {
5 + if info.energy.is_some() { 1 } else { 0 }
} else {
0
}
}) + if self.bubble.is_some() { 13 } else { 0 }
}
}
@ -155,6 +171,7 @@ impl<'a> Widget for Overhead<'a> {
if let Some(Info {
name,
stats,
buffs,
energy,
}) = self.info
{
@ -185,6 +202,84 @@ impl<'a> Widget for Overhead<'a> {
1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
_ => format!("{:.0}M", (health_max as f64 / 1.0e6).max(1.0)),
};
// Buffs
// Alignment
let buff_count = buffs.kinds.len().min(11);
Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT)
.x_y(-1.0, name_y + 60.0)
.parent(id)
.set(state.ids.buffs_align, ui);
let gen = &mut ui.widget_id_generator();
if state.ids.buffs.len() < buff_count {
state.update(|state| state.ids.buffs.resize(buff_count, gen));
};
if state.ids.buff_timers.len() < buff_count {
state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
};
let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; //Animation timer
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);
// Create Buff Widgets
if self.bubble.is_none() {
state
.ids
.buffs
.iter()
.copied()
.zip(state.ids.buff_timers.iter().copied())
.zip(buffs.iter_active().map(get_buff_info))
.enumerate()
.for_each(|(i, ((id, timer_id), buff))| {
// Limit displayed buffs
let max_duration = buff.data.duration;
let current_duration = buff.dur;
let duration_percentage = current_duration.map_or(1000.0, |cur| {
max_duration.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
let buff_img = match buff.kind {
BuffKind::Regeneration { .. } => self.imgs.buff_plus_0,
BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0,
BuffKind::Cursed { .. } => self.imgs.debuff_skull_0,
};
let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
// Sort buffs into rows of 5 slots
let x = i % 5;
let y = i / 5;
let buff_widget = buff_widget.bottom_left_with_margins_on(
state.ids.buffs_align,
0.0 + y as f64 * (21.0),
0.0 + x as f64 * (21.0),
);
buff_widget
.color(
if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
Some(pulsating_col)
} else {
Some(norm_col)
},
)
.set(id, ui);
Image::new(match duration_percentage as u64 {
875..=1000 => self.imgs.nothing, // 8/8
750..=874 => self.imgs.buff_0, // 7/8
625..=749 => self.imgs.buff_1, // 6/8
500..=624 => self.imgs.buff_2, // 5/8
375..=499 => self.imgs.buff_3, // 4/8
250..=374 => self.imgs.buff_4, // 3/8
125..=249 => self.imgs.buff_5, // 2/8
0..=124 => self.imgs.buff_6, // 1/8
_ => self.imgs.nothing,
})
.w_h(20.0, 20.0)
.middle_of(id)
.set(timer_id, ui);
});
}
// Name
Text::new(name)
.font_id(self.fonts.cyri.conrod_id)
@ -254,7 +349,7 @@ impl<'a> Widget for Overhead<'a> {
Rectangle::fill_with(
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
MANA_COLOR,
STAMINA_COLOR,
)
.x_y(
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,

@ -1,9 +1,10 @@
use super::{
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar,
CRITICAL_HP_COLOR, ERROR_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, MENU_BG,
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show,
CRITICAL_HP_COLOR, ERROR_COLOR, HP_COLOR, LOW_HP_COLOR, MENU_BG, STAMINA_COLOR,
TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
hud::BuffPosition,
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode},
ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton},
@ -19,7 +20,6 @@ use conrod_core::{
};
use core::convert::TryFrom;
use inline_tweak::*;
use itertools::Itertools;
use std::iter::once;
use winit::monitor::VideoMode;
@ -159,6 +159,7 @@ widget_ids! {
sfx_volume_text,
audio_device_list,
audio_device_text,
//
hotbar_title,
bar_numbers_title,
show_bar_numbers_none_button,
@ -167,18 +168,20 @@ widget_ids! {
show_bar_numbers_values_text,
show_bar_numbers_percentage_button,
show_bar_numbers_percentage_text,
//
show_shortcuts_button,
show_shortcuts_text,
show_xpbar_button,
show_xpbar_text,
show_bars_button,
show_bars_text,
placeholder,
buff_pos_bar_button,
buff_pos_bar_text,
buff_pos_map_button,
buff_pos_map_text,
//
chat_transp_title,
chat_transp_text,
chat_transp_slider,
chat_char_name_text,
chat_char_name_button,
//
sct_title,
sct_show_text,
sct_show_radio,
@ -195,6 +198,7 @@ widget_ids! {
sct_num_dur_text,
sct_num_dur_slider,
sct_num_dur_value,
//
speech_bubble_text,
speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button,
@ -259,9 +263,9 @@ pub enum Event {
ToggleHelp,
ToggleDebug,
ToggleTips(bool),
ToggleXpBar(XpBar),
ToggleBarNumbers(BarNumbers),
ToggleShortcutNumbers(ShortcutNumbers),
BuffPosition(BuffPosition),
ChangeTab(SettingsTab),
Close,
AdjustMousePan(u32),
@ -796,40 +800,6 @@ impl<'a> Widget for SettingsWindow<'a> {
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.hotbar_title, ui);
// Show xp bar
if Button::image(match self.global_state.settings.gameplay.xp_bar {
XpBar::Always => self.imgs.checkbox_checked,
XpBar::OnGain => self.imgs.checkbox,
})
.w_h(18.0, 18.0)
.hover_image(match self.global_state.settings.gameplay.xp_bar {
XpBar::Always => self.imgs.checkbox_checked_mo,
XpBar::OnGain => self.imgs.checkbox_mo,
})
.press_image(match self.global_state.settings.gameplay.xp_bar {
XpBar::Always => self.imgs.checkbox_checked,
XpBar::OnGain => self.imgs.checkbox_press,
})
.down_from(state.ids.hotbar_title, 8.0)
.set(state.ids.show_xpbar_button, ui)
.was_clicked()
{
match self.global_state.settings.gameplay.xp_bar {
XpBar::Always => events.push(Event::ToggleXpBar(XpBar::OnGain)),
XpBar::OnGain => events.push(Event::ToggleXpBar(XpBar::Always)),
}
}
Text::new(
&self
.localized_strings
.get("hud.settings.toggle_bar_experience"),
)
.right_from(state.ids.show_xpbar_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_xpbar_button)
.color(TEXT_COLOR)
.set(state.ids.show_xpbar_text, ui);
// Show Shortcut Numbers
if Button::image(match self.global_state.settings.gameplay.shortcut_numbers {
ShortcutNumbers::On => self.imgs.checkbox_checked,
@ -844,7 +814,7 @@ impl<'a> Widget for SettingsWindow<'a> {
ShortcutNumbers::On => self.imgs.checkbox_checked,
ShortcutNumbers::Off => self.imgs.checkbox_press,
})
.down_from(state.ids.show_xpbar_button, 8.0)
.down_from(state.ids.hotbar_title, 8.0)
.set(state.ids.show_shortcuts_button, ui)
.was_clicked()
{
@ -864,11 +834,61 @@ impl<'a> Widget for SettingsWindow<'a> {
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.show_shortcuts_text, ui);
Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT)
.down_from(state.ids.show_shortcuts_text, 30.0)
.set(state.ids.placeholder, ui);
// Buff Position
// Buffs above skills
if Button::image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked,
BuffPosition::Map => self.imgs.checkbox,
})
.w_h(18.0, 18.0)
.hover_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked_mo,
BuffPosition::Map => self.imgs.checkbox_mo,
})
.press_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Bar => self.imgs.checkbox_checked,
BuffPosition::Map => self.imgs.checkbox_press,
})
.down_from(state.ids.show_shortcuts_button, 8.0)
.set(state.ids.buff_pos_bar_button, ui)
.was_clicked()
{
events.push(Event::BuffPosition(BuffPosition::Bar))
}
Text::new(&self.localized_strings.get("hud.settings.buffs_skillbar"))
.right_from(state.ids.buff_pos_bar_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.buff_pos_bar_text, ui);
// Buffs left from minimap
if Button::image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked,
BuffPosition::Bar => self.imgs.checkbox,
})
.w_h(18.0, 18.0)
.hover_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked_mo,
BuffPosition::Bar => self.imgs.checkbox_mo,
})
.press_image(match self.global_state.settings.gameplay.buff_position {
BuffPosition::Map => self.imgs.checkbox_checked,
BuffPosition::Bar => self.imgs.checkbox_press,
})
.down_from(state.ids.buff_pos_bar_button, 8.0)
.set(state.ids.buff_pos_map_button, ui)
.was_clicked()
{
events.push(Event::BuffPosition(BuffPosition::Map))
}
Text::new(&self.localized_strings.get("hud.settings.buffs_mmap"))
.right_from(state.ids.buff_pos_map_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR)
.set(state.ids.buff_pos_map_text, ui);
// Content Right Side
/*Scrolling Combat text
@ -1692,7 +1712,7 @@ impl<'a> Widget for SettingsWindow<'a> {
0..=14 => CRITICAL_HP_COLOR,
15..=29 => LOW_HP_COLOR,
30..=50 => HP_COLOR,
_ => MANA_COLOR,
_ => STAMINA_COLOR,
};
Text::new(&format!("FPS: {:.0}", self.fps))
.color(fps_col)
@ -2688,8 +2708,8 @@ impl<'a> Widget for SettingsWindow<'a> {
});
};
for (i, language) in language_list.iter().enumerate() {
let button_w = tweak!(400.0);
let button_h = tweak!(50.0);
let button_w = 400.0;
let button_h = 50.0;
let button = Button::image(if selected_language == &language.language_identifier {
self.imgs.selection
} else {
@ -2706,7 +2726,7 @@ impl<'a> Widget for SettingsWindow<'a> {
.hover_image(self.imgs.selection_hover)
.press_image(self.imgs.selection_press)
.label_color(TEXT_COLOR)
.label_font_size(self.fonts.cyri.scale(tweak!(22)))
.label_font_size(self.fonts.cyri.scale(22))
.label_font_id(self.fonts.cyri.conrod_id)
.label_y(conrod_core::position::Relative::Scalar(2.0))
.set(state.ids.language_list[i], ui)

File diff suppressed because it is too large Load Diff

@ -18,7 +18,6 @@ use conrod_core::{
widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
};
use image::DynamicImage;
//use inline_tweak::*;
use rand::{seq::SliceRandom, thread_rng, Rng};
use std::time::Duration;

@ -894,6 +894,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.shortcut_numbers = shortcut_numbers;
global_state.settings.save_to_file_warn();
},
HudEvent::BuffPosition(buff_position) => {
global_state.settings.gameplay.buff_position = buff_position;
global_state.settings.save_to_file_warn();
},
HudEvent::UiScale(scale_change) => {
global_state.settings.gameplay.ui_scale =
self.hud.scale_change(scale_change);
@ -921,6 +925,10 @@ impl PlayState for SessionState {
global_state.settings.graphics.max_fps = fps;
global_state.settings.save_to_file_warn();
},
HudEvent::RemoveBuff(buff_id) => {
let mut client = self.client.borrow_mut();
client.remove_buff(buff_id);
},
HudEvent::UseSlot(x) => self.client.borrow_mut().use_slot(x),
HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b),
HudEvent::DropSlot(x) => {

@ -1,5 +1,5 @@
use crate::{
hud::{BarNumbers, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
hud::{BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
i18n,
render::RenderMode,
ui::ScaleMode,
@ -507,6 +507,7 @@ pub struct GameplaySettings {
pub intro_show: Intro,
pub xp_bar: XpBar,
pub shortcut_numbers: ShortcutNumbers,
pub buff_position: BuffPosition,
pub bar_numbers: BarNumbers,
pub ui_scale: ScaleMode,
pub free_look_behavior: PressBehavior,
@ -537,6 +538,7 @@ impl Default for GameplaySettings {
intro_show: Intro::Show,
xp_bar: XpBar::Always,
shortcut_numbers: ShortcutNumbers::On,
buff_position: BuffPosition::Map,
bar_numbers: BarNumbers::Values,
ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()),
free_look_behavior: PressBehavior::Toggle,