Merge branch 'BottledByte/buff_system' into 'master'

The Buff system

Closes #413

See merge request veloren/veloren!1285
This commit is contained in:
Samuel Keiffer 2020-10-27 17:11:02 +00:00
commit 19e210672a
70 changed files with 2191 additions and 883 deletions

View File

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

View File

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

View File

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

View File

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

BIN
assets/voxygen/element/animation/buff_frame/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/3.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/4.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/5.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/6.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/7.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/animation/buff_frame/8.png (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
assets/voxygen/element/icons/m1.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/icons/m2.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/skillbar/bg.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/skillbar/frame.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

BIN
assets/voxygen/voxel/armor/back/backpack-0.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/armor/back/backpack-grey.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: (),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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