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
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 - Gave the axe a third attack
- A new secondary charged melee attack for the hammer - A new secondary charged melee attack for the hammer
- Added Dutch translations - Added Dutch translations
- Buff system
### Changed ### Changed

View File

@ -1,6 +1,6 @@
ItemDef( ItemDef(
name: "Apple Stick", name: "Apple Stick",
description: "Restores 20 Health", description: "Restores 25 Health",
kind: Consumable( kind: Consumable(
kind: "AppleStick", kind: "AppleStick",
effect: Health(( 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_melee_kill_msg": "[{attacker}] defeated [{victim}]",
"hud.chat.pvp_ranged_kill_msg": "[{attacker}] shot [{victim}]", "hud.chat.pvp_ranged_kill_msg": "[{attacker}] shot [{victim}]",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] blew up [{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_melee_kill_msg": "{attacker} killed [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]", "hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]",
@ -290,6 +291,8 @@ magically infused items?"#,
"hud.settings.transparency": "Transparency", "hud.settings.transparency": "Transparency",
"hud.settings.hotbar": "Hotbar", "hud.settings.hotbar": "Hotbar",
"hud.settings.toggle_shortcuts": "Toggle Shortcuts", "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.toggle_bar_experience": "Toggle Experience Bar",
"hud.settings.scrolling_combat_text": "Scrolling Combat Text", "hud.settings.scrolling_combat_text": "Scrolling Combat Text",
"hud.settings.single_damage_number": "Single Damage Numbers", "hud.settings.single_damage_number": "Single Damage Numbers",
@ -342,9 +345,9 @@ magically infused items?"#,
"hud.settings.refresh_rate": "Refresh Rate", "hud.settings.refresh_rate": "Refresh Rate",
"hud.settings.save_window_size": "Save window size", "hud.settings.save_window_size": "Save window size",
"hud.settings.lighting_rendering_mode": "Lighting Rendering Mode", "hud.settings.lighting_rendering_mode": "Lighting Rendering Mode",
"hud.settings.lighting_rendering_mode.ashikhmin": "Type A", "hud.settings.lighting_rendering_mode.ashikhmin": "Type A - High ",
"hud.settings.lighting_rendering_mode.blinnphong": "Type B", "hud.settings.lighting_rendering_mode.blinnphong": "Type B - Medium",
"hud.settings.lighting_rendering_mode.lambertian": "Type L", "hud.settings.lighting_rendering_mode.lambertian": "Type L - Cheap",
"hud.settings.shadow_rendering_mode": "Shadow Rendering Mode", "hud.settings.shadow_rendering_mode": "Shadow Rendering Mode",
"hud.settings.shadow_rendering_mode.none": "None", "hud.settings.shadow_rendering_mode.none": "None",
"hud.settings.shadow_rendering_mode.cheap": "Cheap", "hud.settings.shadow_rendering_mode.cheap": "Cheap",
@ -508,6 +511,16 @@ Protection
"esc_menu.quit_game": "Quit Game", "esc_menu.quit_game": "Quit Game",
/// End Escape Menu Section /// 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.create": "Crear",
"common.okay": "Ok", "common.okay": "Ok",
"common.accept": "Aceptar", "common.accept": "Aceptar",
"common.decline": "Rechazar",
"common.disclaimer": "Cuidado", "common.disclaimer": "Cuidado",
"common.cancel": "Cancelar", "common.cancel": "Cancelar",
"common.none": "Ninguno", "common.none": "Ninguno",
@ -73,6 +74,13 @@ VoxygenLocalization(
"common.you": "Tu", "common.you": "Tu",
"common.automatic": "Automatico", "common.automatic": "Automatico",
"common.random": "Aleatorio", "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 // Message when connection to the server is lost
"common.connection_lost": r#"Conexión perdida! "common.connection_lost": r#"Conexión perdida!
@ -89,9 +97,11 @@ El cliente está actualizado?"#,
"common.weapons.axe": "Hacha", "common.weapons.axe": "Hacha",
"common.weapons.sword": "Espada", "common.weapons.sword": "Espada",
"common.weapons.staff": "Báculo", "common.weapons.staff": "Vara Mágica",
"common.weapons.bow": "Arco", "common.weapons.bow": "Arco",
"common.weapons.hammer": "Martillo", "common.weapons.hammer": "Martillo",
"common.weapons.sceptre": "Cetro curativo",
"common.rand_appearance": "Nombre y Apariencia Aleatoria",
/// End Common section /// End Common section
@ -141,6 +151,9 @@ https://account.veloren.net."#,
"main.login.invalid_character": "El personaje seleccionado no es válido", "main.login.invalid_character": "El personaje seleccionado no es válido",
"main.login.client_crashed": "El cliente crasheó", "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.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 /// End Main screen section
@ -153,6 +166,7 @@ https://account.veloren.net."#,
"hud.waypoint_saved": "Marcador Guardado", "hud.waypoint_saved": "Marcador Guardado",
"hud.press_key_to_show_keybindings_fmt": "Presiona {key} para mostrar los controles del teclado", "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_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_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", "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 // Chat outputs
"hud.chat.online_msg": "[{name}] se ha conectado.", "hud.chat.online_msg": "[{name}] se ha conectado.",
"hud.chat.offline_msg": "[{name}] se ha desconectado.", "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_msg": "Recogiste [{item}]",
"hud.chat.loot_fail": "Tu inventario está lleno!", "hud.chat.loot_fail": "Tu inventario está lleno!",
"hud.chat.goodbye": "Adiós!", "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. 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! Quieres liberar tu cursor para cerrar esta ventana? Presiona TAB!
@ -232,6 +261,7 @@ objetos infundidos con magia?"#,
"hud.bag.chest": "Torso", "hud.bag.chest": "Torso",
"hud.bag.hands": "Manos", "hud.bag.hands": "Manos",
"hud.bag.lantern": "Linterna", "hud.bag.lantern": "Linterna",
"hud.bag.glider": "Planeador",
"hud.bag.belt": "Cinturón", "hud.bag.belt": "Cinturón",
"hud.bag.ring": "Anillo", "hud.bag.ring": "Anillo",
"hud.bag.back": "Espalda", "hud.bag.back": "Espalda",
@ -282,8 +312,8 @@ objetos infundidos con magia?"#,
"hud.settings.invert_scroll_zoom": "Invertir Desplazamiento de Zoom", "hud.settings.invert_scroll_zoom": "Invertir Desplazamiento de Zoom",
"hud.settings.invert_mouse_y_axis": "Invertir eje Y del Ratón", "hud.settings.invert_mouse_y_axis": "Invertir eje Y del Ratón",
"hud.settings.enable_mouse_smoothing": "Suavizado de la Cámara", "hud.settings.enable_mouse_smoothing": "Suavizado de la Cámara",
"hud.settings.free_look_behavior": "Comportamiento de vista libre", "hud.settings.free_look_behavior": "Modo de vista libre",
"hud.settings.auto_walk_behavior": "Comportamiento al caminar automaticamente", "hud.settings.auto_walk_behavior": "Modo de caminata automática",
"hud.settings.stop_auto_walk_on_input": "Frenar caminata automática", "hud.settings.stop_auto_walk_on_input": "Frenar caminata automática",
"hud.settings.view_distance": "Distancia de Visión", "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.maximum_fps": "FPS Máximos",
"hud.settings.fov": "Campo de Visión (grados)", "hud.settings.fov": "Campo de Visión (grados)",
"hud.settings.gamma": "Gama", "hud.settings.gamma": "Gama",
"hud.settings.ambiance": "Brillo del Ambiente",
"hud.settings.antialiasing_mode": "Modo Anti-Aliasing", "hud.settings.antialiasing_mode": "Modo Anti-Aliasing",
"hud.settings.cloud_rendering_mode": "Modo de Renderizado de Nubes", "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": "Modo de Renderizado del Agua",
"hud.settings.fluid_rendering_mode.cheap": "Barato", "hud.settings.fluid_rendering_mode.cheap": "Bajo",
"hud.settings.fluid_rendering_mode.shiny": "Brillante", "hud.settings.fluid_rendering_mode.shiny": "Alto",
"hud.settings.cloud_rendering_mode.regular": "Regular", "hud.settings.cloud_rendering_mode.regular": "Normal",
"hud.settings.fullscreen": "Pantalla Completa", "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.music_volume": "Volumen de Musica",
"hud.settings.sound_effect_volume": "Volumen de Efectos de Sonido", "hud.settings.sound_effect_volume": "Volumen de Efectos de Sonido",
"hud.settings.audio_device": "Dispositivo de Audio", "hud.settings.audio_device": "Dispositivo de Audio",
"hud.settings.awaitingkey": "Presiona una tecla...", "hud.settings.awaitingkey": "Presiona una tecla...",
"hud.settings.unbound": "Ninguno", "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": "Lista de jugadores",
"hud.social.online": "En Línea", "hud.social.online": "En Línea",
@ -315,6 +366,10 @@ objetos infundidos con magia?"#,
"hud.social.not_yet_available": "Aún no esta disponible", "hud.social.not_yet_available": "Aún no esta disponible",
"hud.social.faction": "Facción", "hud.social.faction": "Facción",
"hud.social.play_online_fmt": "{nb_player} jugador(es) en línea", "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": "Crafteo",
"hud.crafting.recipes": "Recetas", "hud.crafting.recipes": "Recetas",
@ -322,6 +377,19 @@ objetos infundidos con magia?"#,
"hud.crafting.craft": "Fabricar", "hud.crafting.craft": "Fabricar",
"hud.crafting.tool_cata": "Requisitos:", "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", "hud.spell": "Hechizos",
"hud.free_look_indicator": "Vista libre activa", "hud.free_look_indicator": "Vista libre activa",
@ -381,6 +449,9 @@ objetos infundidos con magia?"#,
"gameinput.freelook": "Vista Libre", "gameinput.freelook": "Vista Libre",
"gameinput.autowalk": "Caminata Automática", "gameinput.autowalk": "Caminata Automática",
"gameinput.dance": "Bailar", "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.crafting": "Craftear",
"gameinput.sneak": "Agacharse", "gameinput.sneak": "Agacharse",
"gameinput.swimdown": "Sumergirse", "gameinput.swimdown": "Sumergirse",
@ -423,7 +494,7 @@ objetos infundidos con magia?"#,
Estado Físico Estado Físico
Fuerza de Voluntad Valentía
Protección Protección
"#, "#,

View File

@ -1099,6 +1099,14 @@
"voxel.armor.back.short-2", "voxel.armor.back.short-2",
(0.0, -2.0, 0.0), (-90.0, 180.0, 0.0), 1.0, (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 // Rings
Armor(Ring("Ring0")): Png( Armor(Ring("Ring0")): Png(
"element.icons.ring-0", "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

@ -24,5 +24,13 @@
vox_spec: ("armor.back.short-2", (-5.0, -1.0, -11.0)), vox_spec: ("armor.back.short-2", (-5.0, -1.0, -11.0)),
color: None 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}, terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize},
vol::RectVolSize, vol::RectVolSize,
}; };
use comp::BuffKind;
use futures_executor::block_on; use futures_executor::block_on;
use futures_timer::Delay; use futures_timer::Delay;
use futures_util::{select, FutureExt}; use futures_util::{select, FutureExt};
@ -644,6 +645,12 @@ impl Client {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern)); 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 max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { 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 // 4) Tick the client's LocalState
self.state.tick(dt, add_foreign_systems, true); 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 // 5) Terrain
let pos = self let pos = self
@ -1728,6 +1740,11 @@ impl Client {
alias_of_uid(attacker_uid), alias_of_uid(attacker_uid),
alias_of_uid(victim) 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) => { KillSource::NonPlayer(attacker_name, KillType::Melee) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim)) format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
}, },
@ -1742,6 +1759,9 @@ impl Client {
attacker_name, attacker_name,
alias_of_uid(victim) alias_of_uid(victim)
), ),
KillSource::NonPlayer(attacker_name, KillType::Buff) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
},
KillSource::Environment(environment) => { KillSource::Environment(environment) => {
format!("[{}] died in {}", alias_of_uid(victim), environment) format!("[{}] died in {}", alias_of_uid(victim), environment)
}, },
@ -1767,6 +1787,9 @@ impl Client {
KillSource::Player(attacker_uid, KillType::Energy) => message KillSource::Player(attacker_uid, KillType::Energy) => message
.replace("{attacker}", &alias_of_uid(attacker_uid)) .replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)), .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 KillSource::NonPlayer(attacker_name, KillType::Melee) => message
.replace("{attacker}", attacker_name) .replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)), .replace("{victim}", &alias_of_uid(victim)),
@ -1779,6 +1802,9 @@ impl Client {
KillSource::NonPlayer(attacker_name, KillType::Energy) => message KillSource::NonPlayer(attacker_name, KillType::Energy) => message
.replace("{attacker}", attacker_name) .replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)), .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 KillSource::Environment(environment) => message
.replace("{name}", &alias_of_uid(victim)) .replace("{name}", &alias_of_uid(victim))
.replace("{environment}", environment), .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, Projectile,
Explosion, Explosion,
Energy, Energy,
Buff,
// Projectile(String), TODO: add projectile name when available // 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 serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
@ -37,6 +41,7 @@ pub enum ControlEvent {
Unmount, Unmount,
InventoryManip(InventoryManip), InventoryManip(InventoryManip),
GroupManip(GroupManip), GroupManip(GroupManip),
RemoveBuff(BuffKind),
Respawn, Respawn,
} }

View File

@ -3,6 +3,7 @@ mod admin;
pub mod agent; pub mod agent;
pub mod beam; pub mod beam;
pub mod body; pub mod body;
pub mod buff;
mod character_state; mod character_state;
pub mod chat; pub mod chat;
mod controller; mod controller;
@ -31,6 +32,10 @@ pub use body::{
biped_large, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, humanoid, object, biped_large, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, humanoid, object,
quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, Body, BodyData, 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 character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ pub use chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,

View File

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

View File

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

View File

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

View File

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

View File

@ -601,6 +601,7 @@ impl<'a> System<'a> for Sys {
if let comp::HealthSource::Attack { by } if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(by) } | comp::HealthSource::Projectile { owner: Some(by) }
| comp::HealthSource::Energy { owner: Some(by) } | comp::HealthSource::Energy { owner: Some(by) }
| comp::HealthSource::Buff { owner: Some(by) }
| comp::HealthSource::Explosion { owner: Some(by) } = | comp::HealthSource::Explosion { owner: Some(by) } =
my_stats.health.last_change.1.cause 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::{ use crate::{
comp::{ comp::{
group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, buff, group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange,
Loadout, Ori, Pos, Scale, Stats, HealthSource, Loadout, Ori, Pos, Scale, Stats,
}, },
event::{EventBus, LocalEvent, ServerEvent}, event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics, metrics::SysMetrics,
@ -9,7 +9,9 @@ use crate::{
sync::Uid, sync::Uid,
util::Dir, util::Dir,
}; };
use rand::{thread_rng, Rng};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*; use vek::*;
pub const BLOCK_EFFICIENCY: f32 = 0.9; pub const BLOCK_EFFICIENCY: f32 = 0.9;
@ -150,6 +152,24 @@ impl<'a> System<'a> for Sys {
cause, 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; attack.hit_count += 1;
} }
if attack.knockback != 0.0 && damage.healthchange != 0.0 { if attack.knockback != 0.0 && damage.healthchange != 0.0 {

View File

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

View File

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

View File

@ -6,7 +6,7 @@ use crate::{
use common::{ use common::{
assets::Asset, assets::Asset,
comp::{ comp::{
self, self, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource, Item, object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource, Item,
Player, Pos, Stats, Player, Pos, Stats,
@ -165,11 +165,34 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Energy) 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::World => KillSource::FallDamage,
HealthSource::Suicide => KillSource::Suicide, HealthSource::Suicide => KillSource::Suicide,
HealthSource::Projectile { owner: None } HealthSource::Projectile { owner: None }
| HealthSource::Explosion { owner: None } | HealthSource::Explosion { owner: None }
| HealthSource::Energy { owner: None } | HealthSource::Energy { owner: None }
| HealthSource::Buff { owner: None }
| HealthSource::Revive | HealthSource::Revive
| HealthSource::Command | HealthSource::Command
| HealthSource::LevelUp | HealthSource::LevelUp
@ -189,6 +212,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
let by = if let HealthSource::Attack { by } let by = if let HealthSource::Attack { by }
| HealthSource::Projectile { owner: Some(by) } | HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) } | HealthSource::Energy { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) } = cause | HealthSource::Explosion { owner: Some(by) } = cause
{ {
by by
@ -674,3 +698,65 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
PlayerListUpdate::LevelChange(*uid, new_level), 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, handle_loaded_character_data, handle_shockwave, handle_shoot,
}; };
use entity_manipulation::{ use entity_manipulation::{
handle_damage, handle_destroy, handle_explosion, handle_knockback, handle_land_on_ground, handle_buff, handle_damage, handle_destroy, handle_explosion, handle_knockback,
handle_level_up, handle_respawn, handle_land_on_ground, handle_level_up, handle_respawn,
}; };
use group_manip::handle_group; use group_manip::handle_group;
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
@ -133,6 +133,10 @@ impl Server {
ServerEvent::Chat(msg) => { ServerEvent::Chat(msg) => {
chat_messages.push(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::Gravity(1.0))
.with(comp::CharacterState::default()) .with(comp::CharacterState::default())
.with(loadout) .with(loadout)
.with(comp::Buffs::default())
} }
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
@ -202,6 +203,7 @@ impl StateExt for State {
entity, entity,
comp::Alignment::Owned(self.read_component_copied(entity).unwrap()), comp::Alignment::Owned(self.read_component_copied(entity).unwrap()),
); );
self.write_component(entity, comp::Buffs::default());
// Make sure physics components are updated // Make sure physics components are updated
self.write_component(entity, comp::ForceUpdate); self.write_component(entity, comp::ForceUpdate);

View File

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

View File

@ -76,6 +76,7 @@ impl<'a> System<'a> for Sys {
| HealthSource::Projectile { owner: Some(by) } | HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) } | HealthSource::Energy { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) } | HealthSource::Explosion { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Healing { by: Some(by) } => { | HealthSource::Healing { by: Some(by) } => {
let by_me = my_uid.map_or(false, |&uid| by == uid); let by_me = my_uid.map_or(false, |&uid| by == uid);
// If the attack was by me also reset this timer // 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 .localized_strings
.get("hud.chat.pvp_energy_kill_msg") .get("hud.chat.pvp_energy_kill_msg")
.to_string(), .to_string(),
KillSource::Player(_, KillType::Buff) => self
.localized_strings
.get("hud.chat.pvp_buff_kill_msg")
.to_string(),
KillSource::NonPlayer(_, KillType::Melee) => self KillSource::NonPlayer(_, KillType::Melee) => self
.localized_strings .localized_strings
.get("hud.chat.npc_melee_kill_msg") .get("hud.chat.npc_melee_kill_msg")
@ -389,6 +393,10 @@ impl<'a> Widget for Chat<'a> {
.localized_strings .localized_strings
.get("hud.chat.npc_energy_kill_msg") .get("hud.chat.npc_energy_kill_msg")
.to_string(), .to_string(),
KillSource::NonPlayer(_, KillType::Buff) => self
.localized_strings
.get("hud.chat.npc_buff_kill_msg")
.to_string(),
KillSource::Environment(_) => self KillSource::Environment(_) => self
.localized_strings .localized_strings
.get("hud.chat.environmental_kill_msg") .get("hud.chat.environmental_kill_msg")

View File

@ -1,15 +1,20 @@
use super::{ use super::{
img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, img_ids::{Imgs, ImgsRot},
MANA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, 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::{ use crate::{
i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, hud::get_buff_info,
window::GameInput, GlobalState, i18n::VoxygenLocalization,
settings::Settings,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
window::GameInput,
GlobalState,
}; };
use client::{self, Client}; use client::{self, Client};
use common::{ use common::{
comp::{group::Role, Stats}, comp::{group::Role, BuffKind, Stats},
sync::{Uid, WorldSyncExt}, sync::{Uid, WorldSyncExt},
}; };
use conrod_core::{ use conrod_core::{
@ -19,7 +24,6 @@ use conrod_core::{
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
}; };
use specs::{saveload::MarkerAllocator, WorldExt}; use specs::{saveload::MarkerAllocator, WorldExt};
widget_ids! { widget_ids! {
pub struct Ids { pub struct Ids {
group_button, group_button,
@ -44,6 +48,8 @@ widget_ids! {
member_panels_txt[], member_panels_txt[],
member_health[], member_health[],
member_stam[], member_stam[],
buffs[],
buff_timers[],
dead_txt[], dead_txt[],
health_txt[], health_txt[],
timeout_bg, timeout_bg,
@ -63,10 +69,12 @@ pub struct Group<'a> {
client: &'a Client, client: &'a Client,
settings: &'a Settings, settings: &'a Settings,
imgs: &'a Imgs, imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>, localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32, pulse: f32,
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -79,20 +87,24 @@ impl<'a> Group<'a> {
client: &'a Client, client: &'a Client,
settings: &'a Settings, settings: &'a Settings,
imgs: &'a Imgs, imgs: &'a Imgs,
rot_imgs: &'a ImgsRot,
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>, localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
pulse: f32, pulse: f32,
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager,
) -> Self { ) -> Self {
Self { Self {
show, show,
client, client,
settings, settings,
imgs, imgs,
rot_imgs,
fonts, fonts,
localized_strings, localized_strings,
pulse, pulse,
global_state, global_state,
tooltip_manager,
common: widget::CommonBuilder::default(), 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 #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event { fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args; let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new(); 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 // Don't show pets
let group_members = self let group_members = self
@ -293,31 +323,35 @@ impl<'a> Widget for Group<'a> {
let client_state = self.client.state(); let client_state = self.client.state();
let stats = client_state.ecs().read_storage::<common::comp::Stats>(); let stats = client_state.ecs().read_storage::<common::comp::Stats>();
let energy = client_state.ecs().read_storage::<common::comp::Energy>(); 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 let uid_allocator = client_state
.ecs() .ecs()
.read_resource::<common::sync::UidAllocator>(); .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() { for (i, &uid) in group_members.iter().copied().enumerate() {
self.show.group = true; self.show.group = true;
let entity = uid_allocator.retrieve_entity_internal(uid.into()); let entity = uid_allocator.retrieve_entity_internal(uid.into());
let stats = entity.and_then(|entity| stats.get(entity)); let stats = entity.and_then(|entity| stats.get(entity));
let energy = entity.and_then(|entity| energy.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 { if let Some(stats) = stats {
let char_name = stats.name.to_string(); let char_name = stats.name.to_string();
let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64; let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64;
// change panel positions when debug info is shown // 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 { let back = if i == 0 {
Image::new(self.imgs.member_bg) Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0) .top_left_with_margins_on(ui.window, offset, 20.0)
} else { } else {
Image::new(self.imgs.member_bg) 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 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); let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
@ -404,25 +438,132 @@ impl<'a> Widget for Group<'a> {
// Stamina // Stamina
Image::new(self.imgs.bar_content) Image::new(self.imgs.bar_content)
.w_h(100.0 * stam_perc, 8.0) .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) .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
.set(state.ids.member_stam[i], ui); .set(state.ids.member_stam[i], ui);
} }
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 { } else {
// Values N.A. // Values N.A.
if let Some(stats) = stats {
Text::new(&stats.name.to_string()) Text::new(&stats.name.to_string())
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0) .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
.font_size(20) .font_size(20)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(GROUP_COLOR) .color(GROUP_COLOR)
.set(state.ids.member_panels_txt[i], ui); .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 { let back = if i == 0 {
Image::new(self.imgs.member_bg) Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0) .top_left_with_margins_on(ui.window, offset, 20.0)
@ -448,6 +589,7 @@ impl<'a> Widget for Group<'a> {
.set(state.ids.dead_txt[i], ui); .set(state.ids.dead_txt[i], ui);
} }
} }
}
if self.show.group_menu { if self.show.group_menu {
let selected = state.selected_member; let selected = state.selected_member;

View File

@ -148,21 +148,11 @@ image_ids! {
// Skillbar // Skillbar
level_up: "voxygen.element.misc_bg.level_up", level_up: "voxygen.element.misc_bg.level_up",
level_down:"voxygen.element.misc_bg.level_down", 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",
bar_content: "voxygen.element.skillbar.bar_content", bar_content: "voxygen.element.skillbar.bar_content",
skillbar_slot_big: "voxygen.element.skillbar.skillbar_slot_big", skillbar_bg: "voxygen.element.skillbar.bg",
skillbar_slot_big_bg: "voxygen.element.skillbar.skillbar_slot_big", skillbar_frame: "voxygen.element.skillbar.frame",
skillbar_slot_big_act: "voxygen.element.skillbar.skillbar_slot_big", m1_ico: "voxygen.element.icons.m1",
skillbar_slot: "voxygen.element.skillbar.skillbar_slot", m2_ico: "voxygen.element.icons.m2",
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",
// Other Icons/Art // Other Icons/Art
skull: "voxygen.element.icons.skull", skull: "voxygen.element.icons.skull",
@ -282,6 +272,7 @@ image_ids! {
hammerleap: "voxygen.element.icons.skill_hammerleap", hammerleap: "voxygen.element.icons.skill_hammerleap",
skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash", skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash",
skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst", skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst",
missing_icon: "voxygen.element.icons.missing_icon_grey",
// Buttons // Buttons
button: "voxygen.element.buttons.button", button: "voxygen.element.buttons.button",
@ -359,6 +350,24 @@ image_ids! {
chat_tell: "voxygen.element.icons.chat.tell", chat_tell: "voxygen.element.icons.chat.tell",
chat_world: "voxygen.element.icons.chat.world", 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> <BlankGraphic>
nothing: (), nothing: (),
} }

View File

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

View File

@ -1,4 +1,5 @@
mod bag; mod bag;
mod buffs;
mod buttons; mod buttons;
mod chat; mod chat;
mod crafting; mod crafting;
@ -24,6 +25,7 @@ pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState};
pub use settings_window::ScaleChange; pub use settings_window::ScaleChange;
use bag::Bag; use bag::Bag;
use buffs::BuffsBar;
use buttons::Buttons; use buttons::Buttons;
use chat::Chat; use chat::Chat;
use chrono::NaiveTime; use chrono::NaiveTime;
@ -58,7 +60,10 @@ use client::Client;
use common::{ use common::{
assets::Asset, assets::Asset,
comp, comp,
comp::item::{ItemDesc, Quality}, comp::{
item::{ItemDesc, Quality},
BuffKind,
},
span, span,
sync::Uid, sync::Uid,
terrain::TerrainChunk, 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 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 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 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 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 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 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 // Item Quality Colors
const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors 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, spell,
skillbar, skillbar,
buttons, buttons,
buffs,
esc_menu, esc_menu,
small_window, small_window,
social_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 struct DebugInfo {
pub tps: f64, pub tps: f64,
pub frame_time: Duration, pub frame_time: Duration,
@ -315,6 +331,7 @@ pub enum Event {
ChatTransp(f32), ChatTransp(f32),
ChatCharName(bool), ChatCharName(bool),
CrosshairType(CrosshairType), CrosshairType(CrosshairType),
BuffPosition(BuffPosition),
ToggleXpBar(XpBar), ToggleXpBar(XpBar),
Intro(Intro), Intro(Intro),
ToggleBarNumbers(BarNumbers), ToggleBarNumbers(BarNumbers),
@ -348,6 +365,7 @@ pub enum Event {
KickMember(common::sync::Uid), KickMember(common::sync::Uid),
LeaveGroup, LeaveGroup,
AssignLeader(common::sync::Uid), AssignLeader(common::sync::Uid),
RemoveBuff(BuffKind),
} }
// TODO: Are these the possible layouts we want? // TODO: Are these the possible layouts we want?
@ -388,6 +406,13 @@ pub enum ShortcutNumbers {
On, On,
Off, Off,
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum BuffPosition {
Bar,
Map,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum PressBehavior { pub enum PressBehavior {
Toggle = 0, Toggle = 0,
@ -722,6 +747,7 @@ impl Hud {
let ecs = client.state().ecs(); let ecs = client.state().ecs();
let pos = ecs.read_storage::<comp::Pos>(); let pos = ecs.read_storage::<comp::Pos>();
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>();
let energy = ecs.read_storage::<comp::Energy>(); let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>(); let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
let uids = ecs.read_storage::<common::sync::Uid>(); let uids = ecs.read_storage::<common::sync::Uid>();
@ -1120,11 +1146,12 @@ impl Hud {
let speech_bubbles = &self.speech_bubbles; let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars // 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, &entities,
&pos, &pos,
interpolated.maybe(), interpolated.maybe(),
&stats, &stats,
&buffs,
energy.maybe(), energy.maybe(),
scales.maybe(), scales.maybe(),
&bodies, &bodies,
@ -1138,7 +1165,7 @@ impl Hud {
entity != me && !stats.is_dead entity != me && !stats.is_dead
}) })
.filter_map( .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 // Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos); let pos = interpolated.map_or(pos.0, |i| i.pos);
let in_group = client.group_members().contains_key(uid); let in_group = client.group_members().contains_key(uid);
@ -1168,6 +1195,7 @@ impl Hud {
let info = display_overhead_info.then(|| overhead::Info { let info = display_overhead_info.then(|| overhead::Info {
name: &stats.name, name: &stats.name,
stats, stats,
buffs,
energy, energy,
}); });
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) { let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
@ -1182,6 +1210,7 @@ impl Hud {
info, info,
bubble, bubble,
stats, stats,
buffs,
body.height() * scale.map_or(1.0, |s| s.0) + 0.5, body.height() * scale.map_or(1.0, |s| s.0) + 0.5,
hpfl, hpfl,
in_group, in_group,
@ -1730,6 +1759,7 @@ impl Hud {
// Bag button and nearby icons // Bag button and nearby icons
let ecs = client.state().ecs(); let ecs = client.state().ecs();
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>();
if let Some(player_stats) = stats.get(client.entity()) { if let Some(player_stats) = stats.get(client.entity()) {
match Buttons::new( match Buttons::new(
client, 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 (waypoint saved and similar notifications)
Popup::new( Popup::new(
&self.voxygen_i18n, &self.voxygen_i18n,
@ -1828,8 +1900,8 @@ impl Hud {
Some(stats), Some(stats),
Some(loadout), Some(loadout),
Some(energy), Some(energy),
Some(character_state), Some(_character_state),
Some(controller), Some(_controller),
Some(inventory), Some(inventory),
) = ( ) = (
stats.get(entity), stats.get(entity),
@ -1848,9 +1920,9 @@ impl Hud {
&stats, &stats,
&loadout, &loadout,
&energy, &energy,
&character_state, //&character_state,
self.pulse, self.pulse,
&controller, //&controller,
&inventory, &inventory,
&self.hotbar, &self.hotbar,
tooltip_manager, tooltip_manager,
@ -1996,6 +2068,9 @@ impl Hud {
settings_window::Event::ToggleZoomInvert(zoom_inverted) => { settings_window::Event::ToggleZoomInvert(zoom_inverted) => {
events.push(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) => { settings_window::Event::ToggleMouseYInvert(mouse_y_inverted) => {
events.push(Event::ToggleMouseYInvert(mouse_y_inverted)); events.push(Event::ToggleMouseYInvert(mouse_y_inverted));
}, },
@ -2032,9 +2107,6 @@ impl Hud {
settings_window::Event::CrosshairType(crosshair_type) => { settings_window::Event::CrosshairType(crosshair_type) => {
events.push(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) => { settings_window::Event::ToggleBarNumbers(bar_numbers) => {
events.push(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 // Spellbook
if self.show.spell { if self.show.spell {
@ -2675,3 +2726,12 @@ pub fn get_quality_col<I: ItemDesc>(item: &I) -> Color {
Quality::Debug => QUALITY_DEBUG, 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::{ use super::{
img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR, 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::{ use crate::{
hud::get_buff_info,
i18n::VoxygenLocalization, i18n::VoxygenLocalization,
settings::GameplaySettings, settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable}, ui::{fonts::ConrodVoxygenFonts, Ingameable},
}; };
use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats}; use common::comp::{BuffKind, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats};
use conrod_core::{ use conrod_core::{
color,
position::Align, position::Align,
widget::{self, Image, Rectangle, Text}, widget::{self, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
@ -44,6 +46,11 @@ widget_ids! {
health_txt, health_txt,
mana_bar, mana_bar,
health_bar_fg, health_bar_fg,
// Buffs
buffs_align,
buffs[],
buff_timers[],
} }
} }
@ -51,6 +58,7 @@ widget_ids! {
pub struct Info<'a> { pub struct Info<'a> {
pub name: &'a str, pub name: &'a str,
pub stats: &'a Stats, pub stats: &'a Stats,
pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>, pub energy: Option<&'a Energy>,
} }
@ -119,13 +127,21 @@ impl<'a> Ingameable for Overhead<'a> {
// - 1 for HP text // - 1 for HP text
// - If there's mana // - If there's mana
// - 1 Rect::new for 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 // If there's a speech bubble
// - 2 Text::new for speech bubble // - 2 Text::new for speech bubble
// - 1 Image::new for icon // - 1 Image::new for icon
// - 10 Image::new for speech bubble (9-slice + tail) // - 10 Image::new for speech bubble (9-slice + tail)
self.info.map_or(0, |info| { self.info.map_or(0, |info| {
2 + if show_healthbar(info.stats) { 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 } 5 + if info.energy.is_some() { 1 } else { 0 }
} else { } else {
0 0
@ -155,6 +171,7 @@ impl<'a> Widget for Overhead<'a> {
if let Some(Info { if let Some(Info {
name, name,
stats, stats,
buffs,
energy, energy,
}) = self.info }) = self.info
{ {
@ -185,6 +202,84 @@ impl<'a> Widget for Overhead<'a> {
1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)), 1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
_ => format!("{:.0}M", (health_max as f64 / 1.0e6).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 // Name
Text::new(name) Text::new(name)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
@ -254,7 +349,7 @@ impl<'a> Widget for Overhead<'a> {
Rectangle::fill_with( Rectangle::fill_with(
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT], [72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
MANA_COLOR, STAMINA_COLOR,
) )
.x_y( .x_y(
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE, ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,

View File

@ -1,9 +1,10 @@
use super::{ use super::{
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar, img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show,
CRITICAL_HP_COLOR, ERROR_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, MENU_BG, 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, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
}; };
use crate::{ use crate::{
hud::BuffPosition,
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization}, i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode}, render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode},
ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton}, ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton},
@ -19,7 +20,6 @@ use conrod_core::{
}; };
use core::convert::TryFrom; use core::convert::TryFrom;
use inline_tweak::*;
use itertools::Itertools; use itertools::Itertools;
use std::iter::once; use std::iter::once;
use winit::monitor::VideoMode; use winit::monitor::VideoMode;
@ -159,6 +159,7 @@ widget_ids! {
sfx_volume_text, sfx_volume_text,
audio_device_list, audio_device_list,
audio_device_text, audio_device_text,
//
hotbar_title, hotbar_title,
bar_numbers_title, bar_numbers_title,
show_bar_numbers_none_button, show_bar_numbers_none_button,
@ -167,18 +168,20 @@ widget_ids! {
show_bar_numbers_values_text, show_bar_numbers_values_text,
show_bar_numbers_percentage_button, show_bar_numbers_percentage_button,
show_bar_numbers_percentage_text, show_bar_numbers_percentage_text,
//
show_shortcuts_button, show_shortcuts_button,
show_shortcuts_text, show_shortcuts_text,
show_xpbar_button, buff_pos_bar_button,
show_xpbar_text, buff_pos_bar_text,
show_bars_button, buff_pos_map_button,
show_bars_text, buff_pos_map_text,
placeholder, //
chat_transp_title, chat_transp_title,
chat_transp_text, chat_transp_text,
chat_transp_slider, chat_transp_slider,
chat_char_name_text, chat_char_name_text,
chat_char_name_button, chat_char_name_button,
//
sct_title, sct_title,
sct_show_text, sct_show_text,
sct_show_radio, sct_show_radio,
@ -195,6 +198,7 @@ widget_ids! {
sct_num_dur_text, sct_num_dur_text,
sct_num_dur_slider, sct_num_dur_slider,
sct_num_dur_value, sct_num_dur_value,
//
speech_bubble_text, speech_bubble_text,
speech_bubble_dark_mode_text, speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button, speech_bubble_dark_mode_button,
@ -259,9 +263,9 @@ pub enum Event {
ToggleHelp, ToggleHelp,
ToggleDebug, ToggleDebug,
ToggleTips(bool), ToggleTips(bool),
ToggleXpBar(XpBar),
ToggleBarNumbers(BarNumbers), ToggleBarNumbers(BarNumbers),
ToggleShortcutNumbers(ShortcutNumbers), ToggleShortcutNumbers(ShortcutNumbers),
BuffPosition(BuffPosition),
ChangeTab(SettingsTab), ChangeTab(SettingsTab),
Close, Close,
AdjustMousePan(u32), AdjustMousePan(u32),
@ -796,40 +800,6 @@ impl<'a> Widget for SettingsWindow<'a> {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(state.ids.hotbar_title, ui); .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 // Show Shortcut Numbers
if Button::image(match self.global_state.settings.gameplay.shortcut_numbers { if Button::image(match self.global_state.settings.gameplay.shortcut_numbers {
ShortcutNumbers::On => self.imgs.checkbox_checked, ShortcutNumbers::On => self.imgs.checkbox_checked,
@ -844,7 +814,7 @@ impl<'a> Widget for SettingsWindow<'a> {
ShortcutNumbers::On => self.imgs.checkbox_checked, ShortcutNumbers::On => self.imgs.checkbox_checked,
ShortcutNumbers::Off => self.imgs.checkbox_press, 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) .set(state.ids.show_shortcuts_button, ui)
.was_clicked() .was_clicked()
{ {
@ -864,11 +834,61 @@ impl<'a> Widget for SettingsWindow<'a> {
.graphics_for(state.ids.show_shortcuts_button) .graphics_for(state.ids.show_shortcuts_button)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(state.ids.show_shortcuts_text, ui); .set(state.ids.show_shortcuts_text, ui);
// Buff Position
Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT) // Buffs above skills
.down_from(state.ids.show_shortcuts_text, 30.0) if Button::image(match self.global_state.settings.gameplay.buff_position {
.set(state.ids.placeholder, ui); 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 // Content Right Side
/*Scrolling Combat text /*Scrolling Combat text
@ -1692,7 +1712,7 @@ impl<'a> Widget for SettingsWindow<'a> {
0..=14 => CRITICAL_HP_COLOR, 0..=14 => CRITICAL_HP_COLOR,
15..=29 => LOW_HP_COLOR, 15..=29 => LOW_HP_COLOR,
30..=50 => HP_COLOR, 30..=50 => HP_COLOR,
_ => MANA_COLOR, _ => STAMINA_COLOR,
}; };
Text::new(&format!("FPS: {:.0}", self.fps)) Text::new(&format!("FPS: {:.0}", self.fps))
.color(fps_col) .color(fps_col)
@ -2688,8 +2708,8 @@ impl<'a> Widget for SettingsWindow<'a> {
}); });
}; };
for (i, language) in language_list.iter().enumerate() { for (i, language) in language_list.iter().enumerate() {
let button_w = tweak!(400.0); let button_w = 400.0;
let button_h = tweak!(50.0); let button_h = 50.0;
let button = Button::image(if selected_language == &language.language_identifier { let button = Button::image(if selected_language == &language.language_identifier {
self.imgs.selection self.imgs.selection
} else { } else {
@ -2706,7 +2726,7 @@ impl<'a> Widget for SettingsWindow<'a> {
.hover_image(self.imgs.selection_hover) .hover_image(self.imgs.selection_hover)
.press_image(self.imgs.selection_press) .press_image(self.imgs.selection_press)
.label_color(TEXT_COLOR) .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_font_id(self.fonts.cyri.conrod_id)
.label_y(conrod_core::position::Relative::Scalar(2.0)) .label_y(conrod_core::position::Relative::Scalar(2.0))
.set(state.ids.language_list[i], ui) .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, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
}; };
use image::DynamicImage; use image::DynamicImage;
//use inline_tweak::*;
use rand::{seq::SliceRandom, thread_rng, Rng}; use rand::{seq::SliceRandom, thread_rng, Rng};
use std::time::Duration; 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.gameplay.shortcut_numbers = shortcut_numbers;
global_state.settings.save_to_file_warn(); 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) => { HudEvent::UiScale(scale_change) => {
global_state.settings.gameplay.ui_scale = global_state.settings.gameplay.ui_scale =
self.hud.scale_change(scale_change); self.hud.scale_change(scale_change);
@ -921,6 +925,10 @@ impl PlayState for SessionState {
global_state.settings.graphics.max_fps = fps; global_state.settings.graphics.max_fps = fps;
global_state.settings.save_to_file_warn(); 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::UseSlot(x) => self.client.borrow_mut().use_slot(x),
HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b), HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b),
HudEvent::DropSlot(x) => { HudEvent::DropSlot(x) => {

View File

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