diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c3498d68..734b6c53a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Gave the axe a third attack - A new secondary charged melee attack for the hammer - Added Dutch translations +- Buff system ### Changed diff --git a/assets/common/items/food/apple_stick.ron b/assets/common/items/food/apple_stick.ron index 5bf9454978..7d5071e5f7 100644 --- a/assets/common/items/food/apple_stick.ron +++ b/assets/common/items/food/apple_stick.ron @@ -1,6 +1,6 @@ ItemDef( name: "Apple Stick", - description: "Restores 20 Health", + description: "Restores 25 Health", kind: Consumable( kind: "AppleStick", effect: Health(( diff --git a/assets/common/items/npc_armor/back/backpack_0.ron b/assets/common/items/npc_armor/back/backpack_0.ron new file mode 100644 index 0000000000..00a64c0d47 --- /dev/null +++ b/assets/common/items/npc_armor/back/backpack_0.ron @@ -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, +) diff --git a/assets/common/items/npc_armor/back/backpack_blue_0.ron b/assets/common/items/npc_armor/back/backpack_blue_0.ron new file mode 100644 index 0000000000..66446c93f2 --- /dev/null +++ b/assets/common/items/npc_armor/back/backpack_blue_0.ron @@ -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, +) diff --git a/assets/voxygen/element/animation/buff_frame/1.png b/assets/voxygen/element/animation/buff_frame/1.png new file mode 100644 index 0000000000..e05166cd46 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/1.png differ diff --git a/assets/voxygen/element/animation/buff_frame/2.png b/assets/voxygen/element/animation/buff_frame/2.png new file mode 100644 index 0000000000..54c5183ec1 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/2.png differ diff --git a/assets/voxygen/element/animation/buff_frame/3.png b/assets/voxygen/element/animation/buff_frame/3.png new file mode 100644 index 0000000000..b0a196aeac Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/3.png differ diff --git a/assets/voxygen/element/animation/buff_frame/4.png b/assets/voxygen/element/animation/buff_frame/4.png new file mode 100644 index 0000000000..beceaf397b Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/4.png differ diff --git a/assets/voxygen/element/animation/buff_frame/5.png b/assets/voxygen/element/animation/buff_frame/5.png new file mode 100644 index 0000000000..4cb8dc04c1 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/5.png differ diff --git a/assets/voxygen/element/animation/buff_frame/6.png b/assets/voxygen/element/animation/buff_frame/6.png new file mode 100644 index 0000000000..4bc7af2a36 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/6.png differ diff --git a/assets/voxygen/element/animation/buff_frame/7.png b/assets/voxygen/element/animation/buff_frame/7.png new file mode 100644 index 0000000000..80dcb85b44 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/7.png differ diff --git a/assets/voxygen/element/animation/buff_frame/8.png b/assets/voxygen/element/animation/buff_frame/8.png new file mode 100644 index 0000000000..e158dfbb97 Binary files /dev/null and b/assets/voxygen/element/animation/buff_frame/8.png differ diff --git a/assets/voxygen/element/icons/de_buffs/buff_plus_0.png b/assets/voxygen/element/icons/de_buffs/buff_plus_0.png new file mode 100644 index 0000000000..7ad6751359 Binary files /dev/null and b/assets/voxygen/element/icons/de_buffs/buff_plus_0.png differ diff --git a/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png b/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png new file mode 100644 index 0000000000..4638eeb618 Binary files /dev/null and b/assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png differ diff --git a/assets/voxygen/element/icons/de_buffs/debuff_skull_0.png b/assets/voxygen/element/icons/de_buffs/debuff_skull_0.png new file mode 100644 index 0000000000..1e16dcf7ce Binary files /dev/null and b/assets/voxygen/element/icons/de_buffs/debuff_skull_0.png differ diff --git a/assets/voxygen/element/icons/m1.png b/assets/voxygen/element/icons/m1.png index 774ae38bc5..1b5f113651 100644 Binary files a/assets/voxygen/element/icons/m1.png and b/assets/voxygen/element/icons/m1.png differ diff --git a/assets/voxygen/element/icons/m2.png b/assets/voxygen/element/icons/m2.png index aeb57b3d46..0e346644b9 100644 Binary files a/assets/voxygen/element/icons/m2.png and b/assets/voxygen/element/icons/m2.png differ diff --git a/assets/voxygen/element/skillbar/bar_content.png b/assets/voxygen/element/skillbar/bar_content.png index 61d1f93094..5f27678238 100644 Binary files a/assets/voxygen/element/skillbar/bar_content.png and b/assets/voxygen/element/skillbar/bar_content.png differ diff --git a/assets/voxygen/element/skillbar/bg.png b/assets/voxygen/element/skillbar/bg.png new file mode 100644 index 0000000000..eda76288f4 Binary files /dev/null and b/assets/voxygen/element/skillbar/bg.png differ diff --git a/assets/voxygen/element/skillbar/energybar_bg.png b/assets/voxygen/element/skillbar/energybar_bg.png deleted file mode 100644 index c05ee0e667..0000000000 Binary files a/assets/voxygen/element/skillbar/energybar_bg.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/frame.png b/assets/voxygen/element/skillbar/frame.png new file mode 100644 index 0000000000..8af8fe295e Binary files /dev/null and b/assets/voxygen/element/skillbar/frame.png differ diff --git a/assets/voxygen/element/skillbar/healthbar_bg.png b/assets/voxygen/element/skillbar/healthbar_bg.png deleted file mode 100644 index 3696ab4fd3..0000000000 Binary files a/assets/voxygen/element/skillbar/healthbar_bg.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot.png b/assets/voxygen/element/skillbar/skillbar_slot.png deleted file mode 100644 index 9a132d9882..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_active.png b/assets/voxygen/element/skillbar/skillbar_slot_active.png deleted file mode 100644 index 1cbfb65d5c..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_active.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_big.png b/assets/voxygen/element/skillbar/skillbar_slot_big.png deleted file mode 100644 index c7f27c4897..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_big.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_l.png b/assets/voxygen/element/skillbar/skillbar_slot_l.png deleted file mode 100644 index 5e786baebf..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_l.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_l_active.png b/assets/voxygen/element/skillbar/skillbar_slot_l_active.png deleted file mode 100644 index d146f3d20f..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_l_active.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_r.png b/assets/voxygen/element/skillbar/skillbar_slot_r.png deleted file mode 100644 index d6d6030958..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_r.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/skillbar_slot_r_active.png b/assets/voxygen/element/skillbar/skillbar_slot_r_active.png deleted file mode 100644 index dd4eef2233..0000000000 Binary files a/assets/voxygen/element/skillbar/skillbar_slot_r_active.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/xp_bar_content.png b/assets/voxygen/element/skillbar/xp_bar_content.png deleted file mode 100644 index f44d2432d7..0000000000 Binary files a/assets/voxygen/element/skillbar/xp_bar_content.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/xp_bar_left.png b/assets/voxygen/element/skillbar/xp_bar_left.png deleted file mode 100644 index 4693d465bb..0000000000 Binary files a/assets/voxygen/element/skillbar/xp_bar_left.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/xp_bar_mid.png b/assets/voxygen/element/skillbar/xp_bar_mid.png deleted file mode 100644 index 6558458ce2..0000000000 Binary files a/assets/voxygen/element/skillbar/xp_bar_mid.png and /dev/null differ diff --git a/assets/voxygen/element/skillbar/xp_bar_right.png b/assets/voxygen/element/skillbar/xp_bar_right.png deleted file mode 100644 index c45a6ba988..0000000000 Binary files a/assets/voxygen/element/skillbar/xp_bar_right.png and /dev/null differ diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 04a12e119f..5239007e82 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -183,7 +183,8 @@ https://account.veloren.net."#, "hud.chat.pvp_melee_kill_msg": "[{attacker}] defeated [{victim}]", "hud.chat.pvp_ranged_kill_msg": "[{attacker}] shot [{victim}]", "hud.chat.pvp_explosion_kill_msg": "[{attacker}] blew up [{victim}]", - "hud.chat.pvp_energy_kill_msg": "[{attacker}] used magic to kill [{victim}]", + "hud.chat.pvp_energy_kill_msg": "[{attacker}] killed [{victim}] with magic", + "hud.chat.pvp_buff_kill_msg": "[{attacker}] killed [{victim}]", "hud.chat.npc_melee_kill_msg": "{attacker} killed [{victim}]", "hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]", @@ -290,6 +291,8 @@ magically infused items?"#, "hud.settings.transparency": "Transparency", "hud.settings.hotbar": "Hotbar", "hud.settings.toggle_shortcuts": "Toggle Shortcuts", + "hud.settings.buffs_skillbar": "Buffs at Skillbar", + "hud.settings.buffs_mmap": "Buffs at Minimap", "hud.settings.toggle_bar_experience": "Toggle Experience Bar", "hud.settings.scrolling_combat_text": "Scrolling Combat Text", "hud.settings.single_damage_number": "Single Damage Numbers", @@ -342,9 +345,9 @@ magically infused items?"#, "hud.settings.refresh_rate": "Refresh Rate", "hud.settings.save_window_size": "Save window size", "hud.settings.lighting_rendering_mode": "Lighting Rendering Mode", - "hud.settings.lighting_rendering_mode.ashikhmin": "Type A", - "hud.settings.lighting_rendering_mode.blinnphong": "Type B", - "hud.settings.lighting_rendering_mode.lambertian": "Type L", + "hud.settings.lighting_rendering_mode.ashikhmin": "Type A - High ", + "hud.settings.lighting_rendering_mode.blinnphong": "Type B - Medium", + "hud.settings.lighting_rendering_mode.lambertian": "Type L - Cheap", "hud.settings.shadow_rendering_mode": "Shadow Rendering Mode", "hud.settings.shadow_rendering_mode.none": "None", "hud.settings.shadow_rendering_mode.cheap": "Cheap", @@ -508,6 +511,16 @@ Protection "esc_menu.quit_game": "Quit Game", /// End Escape Menu Section + /// Buffs and Debuffs + "buff.remove": "Click to remove", + "buff.title.missing": "Missing Title", + "buff.desc.missing": "Missing Description", + // Buffs + "buff.title.heal_test": "Heal Test", + "buff.desc.heal_test": "This is a test buff to test healing.", + // Debuffs + "debuff.title.bleed_test": "Bleed Test", + "debuff.desc.bleed_test": "This is a test debuff to test bleeding.", }, diff --git a/assets/voxygen/i18n/es_la.ron b/assets/voxygen/i18n/es_la.ron index 866257d619..8b13d7e628 100644 --- a/assets/voxygen/i18n/es_la.ron +++ b/assets/voxygen/i18n/es_la.ron @@ -65,6 +65,7 @@ VoxygenLocalization( "common.create": "Crear", "common.okay": "Ok", "common.accept": "Aceptar", + "common.decline": "Rechazar", "common.disclaimer": "Cuidado", "common.cancel": "Cancelar", "common.none": "Ninguno", @@ -73,6 +74,13 @@ VoxygenLocalization( "common.you": "Tu", "common.automatic": "Automatico", "common.random": "Aleatorio", + // Settings Window title + "common.interface_settings": "Ajustes de Interfaz", + "common.gameplay_settings": "Ajustes de Jugabilidad", + "common.controls_settings": "Ajustes de Controles", + "common.video_settings": "Ajustes de Graficos", + "common.sound_settings": "Ajustes de Sonido", + "common.language_settings": "Ajustes de Idiomas", // Message when connection to the server is lost "common.connection_lost": r#"Conexión perdida! @@ -89,9 +97,11 @@ El cliente está actualizado?"#, "common.weapons.axe": "Hacha", "common.weapons.sword": "Espada", - "common.weapons.staff": "Báculo", + "common.weapons.staff": "Vara Mágica", "common.weapons.bow": "Arco", "common.weapons.hammer": "Martillo", + "common.weapons.sceptre": "Cetro curativo", + "common.rand_appearance": "Nombre y Apariencia Aleatoria", /// End Common section @@ -141,6 +151,9 @@ https://account.veloren.net."#, "main.login.invalid_character": "El personaje seleccionado no es válido", "main.login.client_crashed": "El cliente crasheó", "main.login.not_on_whitelist": "No estas en la lista. Contacta al Dueño del Servidor si quieres unirte.", + "main.login.banned": "Usted ha sido baneado por la siguiente razón", + "main.login.kicked": "Te han echado por la siguiente razón", + /// End Main screen section @@ -153,6 +166,7 @@ https://account.veloren.net."#, "hud.waypoint_saved": "Marcador Guardado", "hud.press_key_to_show_keybindings_fmt": "Presiona {key} para mostrar los controles del teclado", + "hud.press_key_to_toggle_lantern_fmt": "[{key}] Encender Linterna", "hud.press_key_to_show_debug_info_fmt": "Presiona {key} para mostrar información de depuración", "hud.press_key_to_toggle_keybindings_fmt": "Presiona {key} para alternar los controles del teclado", "hud.press_key_to_toggle_debug_info_fmt": "Presiona {key} para alternar la información de depuración", @@ -160,6 +174,21 @@ https://account.veloren.net."#, // Chat outputs "hud.chat.online_msg": "[{name}] se ha conectado.", "hud.chat.offline_msg": "[{name}] se ha desconectado.", + + "hud.chat.default_death_msg": "[{name}] Murió", + "hud.chat.environmental_kill_msg": "[{name}] Murió en {environment}", + "hud.chat.fall_kill_msg": "[{name}] Murió por el daño de la caída", + "hud.chat.suicide_msg": "[{name}] Murió por heridas autoinfligidas", + + "hud.chat.pvp_melee_kill_msg": "[{attacker}] Derroto a [{victim}]", + "hud.chat.pvp_ranged_kill_msg": "[{attacker}] Le Disparo a [{victim}]", + "hud.chat.pvp_explosion_kill_msg": "[{attacker}] Hizo explotar a [{victim}]", + "hud.chat.pvp_energy_kill_msg": "[{attacker}] usó magia para matar [{victim}]", + + "hud.chat.npc_melee_kill_msg": "{attacker} Mató a [{victim}]", + "hud.chat.npc_ranged_kill_msg": "{attacker} Le Disparo a [{victim}]", + "hud.chat.npc_explosion_kill_msg": "{attacker} Hizo explotar a [{victim}]", + "hud.chat.loot_msg": "Recogiste [{item}]", "hud.chat.loot_fail": "Tu inventario está lleno!", "hud.chat.goodbye": "Adiós!", @@ -197,7 +226,7 @@ Deshazte de ellos haciendo click en ellos y luego arrastralos fuera de la bolsa. Las noches pueden volverse bastante oscuras en Veloren. -Enciende tu farol escribiendo /lantern en el chat o presionando la G. +Enciende tu Linterna escribiendo /lantern en el chat o presionando la G. Quieres liberar tu cursor para cerrar esta ventana? Presiona TAB! @@ -232,6 +261,7 @@ objetos infundidos con magia?"#, "hud.bag.chest": "Torso", "hud.bag.hands": "Manos", "hud.bag.lantern": "Linterna", + "hud.bag.glider": "Planeador", "hud.bag.belt": "Cinturón", "hud.bag.ring": "Anillo", "hud.bag.back": "Espalda", @@ -282,8 +312,8 @@ objetos infundidos con magia?"#, "hud.settings.invert_scroll_zoom": "Invertir Desplazamiento de Zoom", "hud.settings.invert_mouse_y_axis": "Invertir eje Y del Ratón", "hud.settings.enable_mouse_smoothing": "Suavizado de la Cámara", - "hud.settings.free_look_behavior": "Comportamiento de vista libre", - "hud.settings.auto_walk_behavior": "Comportamiento al caminar automaticamente", + "hud.settings.free_look_behavior": "Modo de vista libre", + "hud.settings.auto_walk_behavior": "Modo de caminata automática", "hud.settings.stop_auto_walk_on_input": "Frenar caminata automática", "hud.settings.view_distance": "Distancia de Visión", @@ -292,22 +322,43 @@ objetos infundidos con magia?"#, "hud.settings.maximum_fps": "FPS Máximos", "hud.settings.fov": "Campo de Visión (grados)", "hud.settings.gamma": "Gama", + "hud.settings.ambiance": "Brillo del Ambiente", "hud.settings.antialiasing_mode": "Modo Anti-Aliasing", "hud.settings.cloud_rendering_mode": "Modo de Renderizado de Nubes", - "hud.settings.fluid_rendering_mode": "Modo de Renderizado de Fluidos", - "hud.settings.fluid_rendering_mode.cheap": "Barato", - "hud.settings.fluid_rendering_mode.shiny": "Brillante", - "hud.settings.cloud_rendering_mode.regular": "Regular", + "hud.settings.fluid_rendering_mode": "Modo de Renderizado del Agua", + "hud.settings.fluid_rendering_mode.cheap": "Bajo", + "hud.settings.fluid_rendering_mode.shiny": "Alto", + "hud.settings.cloud_rendering_mode.regular": "Normal", "hud.settings.fullscreen": "Pantalla Completa", - "hud.settings.save_window_size": "Guardar tamaño de la ventana", - + "hud.settings.fullscreen_mode": "Modo de Pantalla Completa", + "hud.settings.fullscreen_mode.exclusive": "Completo", + "hud.settings.fullscreen_mode.borderless": "Con Bordes", + "hud.settings.particles": "Particulas", + "hud.settings.resolution": "Resolución", + "hud.settings.bit_depth": "Profundidad de Bits", + "hud.settings.refresh_rate": "Taza de Refresco", + "hud.settings.save_window_size": " Guardar tamaño de ventana", + "hud.settings.shadow_rendering_mode": "Renderizado de Sombras", + "hud.settings.shadow_rendering_mode.none": "Ninguno", + "hud.settings.shadow_rendering_mode.cheap": "Bajo", + "hud.settings.shadow_rendering_mode.map": "Alto", + "hud.settings.lighting_rendering_mode": "Renderizado de la luz de la Linterna", + "hud.settings.lighting_rendering_mode.ashikhmin": "Tipo A", + "hud.settings.lighting_rendering_mode.blinnphong": "Tipo B", + "hud.settings.lighting_rendering_mode.lambertian": "Tipo L", + "hud.settings.shadow_rendering_mode": "Renderizado de Sombras", + "hud.settings.shadow_rendering_mode.none": "Ninguno", + "hud.settings.shadow_rendering_mode.cheap": "Bajo", + "hud.settings.shadow_rendering_mode.map": "Alto", + "hud.settings.shadow_rendering_mode.map.resolution": "Resolución", + "hud.settings.lod_detail": "Detalle de LoD", "hud.settings.music_volume": "Volumen de Musica", "hud.settings.sound_effect_volume": "Volumen de Efectos de Sonido", "hud.settings.audio_device": "Dispositivo de Audio", "hud.settings.awaitingkey": "Presiona una tecla...", "hud.settings.unbound": "Ninguno", - "hud.settings.reset_keybinds": "Reestablecer a los valores predeterminados", + "hud.settings.reset_keybinds": "Reestablecer Controles", "hud.social": "Lista de jugadores", "hud.social.online": "En Línea", @@ -315,12 +366,29 @@ objetos infundidos con magia?"#, "hud.social.not_yet_available": "Aún no esta disponible", "hud.social.faction": "Facción", "hud.social.play_online_fmt": "{nb_player} jugador(es) en línea", + "hud.social.name": "Nombre", + "hud.social.level": "Nivel", + "hud.social.zone": "Zona", + "hud.social.account": "Cuenta", "hud.crafting": "Crafteo", "hud.crafting.recipes": "Recetas", "hud.crafting.ingredients": "Ingredientes:", "hud.crafting.craft": "Fabricar", "hud.crafting.tool_cata": "Requisitos:", + + "hud.group": "Grupo", + "hud.group.invite_to_join": "{name} Te invito a su Grupo!", + "hud.group.invite": "Invitar", + "hud.group.kick": "Echar", + "hud.group.assign_leader": "Asignar Lider", + "hud.group.leave": "Salir del Grupo", + "hud.group.dead" : "Muerto", + "hud.group.out_of_range": "Fuera de Alcance", + "hud.group.add_friend": "Agregar a Amigos", + "hud.group.link_group": "Conectar Grupos", + "hud.group.in_menu": "Eligiendo Personaje", + "hud.group.members": "Miembros del Grupo", "hud.spell": "Hechizos", @@ -381,6 +449,9 @@ objetos infundidos con magia?"#, "gameinput.freelook": "Vista Libre", "gameinput.autowalk": "Caminata Automática", "gameinput.dance": "Bailar", + "gameinput.select": "Seleccione la Entidad", + "gameinput.acceptgroupinvite": "Aceptar invitación al grupo", + "gameinput.declinegroupinvite": "Rechazar invitación al grupo", "gameinput.crafting": "Craftear", "gameinput.sneak": "Agacharse", "gameinput.swimdown": "Sumergirse", @@ -423,7 +494,7 @@ objetos infundidos con magia?"#, Estado Físico -Fuerza de Voluntad +Valentía Protección "#, diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index fb4b51f230..06677412cc 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -1099,6 +1099,14 @@ "voxel.armor.back.short-2", (0.0, -2.0, 0.0), (-90.0, 180.0, 0.0), 1.0, ), + Armor(Back("Backpack0")): VoxTrans( + "voxel.armor.back.backpack-0", + (0.0, 0.0, 0.0), (-90.0, 0.0, 0.0), 1.0, + ), + Armor(Back("BackpackBlue0")): VoxTrans( + "voxel.armor.back.backpack-0", + (0.0, 0.0, 0.0), (-90.0, 0.0, 0.0), 1.0, + ), // Rings Armor(Ring("Ring0")): Png( "element.icons.ring-0", diff --git a/assets/voxygen/voxel/armor/back/backpack-0.vox b/assets/voxygen/voxel/armor/back/backpack-0.vox new file mode 100644 index 0000000000..32e778562d Binary files /dev/null and b/assets/voxygen/voxel/armor/back/backpack-0.vox differ diff --git a/assets/voxygen/voxel/armor/back/backpack-grey.vox b/assets/voxygen/voxel/armor/back/backpack-grey.vox new file mode 100644 index 0000000000..60e4568aba Binary files /dev/null and b/assets/voxygen/voxel/armor/back/backpack-grey.vox differ diff --git a/assets/voxygen/voxel/humanoid_armor_back_manifest.ron b/assets/voxygen/voxel/humanoid_armor_back_manifest.ron index 8ab7638b62..baea9952be 100644 --- a/assets/voxygen/voxel/humanoid_armor_back_manifest.ron +++ b/assets/voxygen/voxel/humanoid_armor_back_manifest.ron @@ -23,6 +23,14 @@ "Short2": ( vox_spec: ("armor.back.short-2", (-5.0, -1.0, -11.0)), color: None + ), + "Backpack0": ( + vox_spec: ("armor.back.backpack-0", (-7.0, -5.0, -10.0)), + color: None + ), + "BackpackBlue0": ( + vox_spec: ("armor.back.backpack-grey", (-7.0, -5.0, -10.0)), + color: Some((76, 72, 178)) ), }, )) diff --git a/client/src/lib.rs b/client/src/lib.rs index 9824086919..85c77c3e6b 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -37,6 +37,7 @@ use common::{ terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize}, vol::RectVolSize, }; +use comp::BuffKind; use futures_executor::block_on; use futures_timer::Delay; use futures_util::{select, FutureExt}; @@ -644,6 +645,12 @@ impl Client { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern)); } + pub fn remove_buff(&mut self, buff_id: BuffKind) { + self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff( + buff_id, + ))); + } + pub fn max_group_size(&self) -> u32 { self.max_group_size } pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { @@ -977,6 +984,11 @@ impl Client { // 4) Tick the client's LocalState self.state.tick(dt, add_foreign_systems, true); + // TODO: avoid emitting these in the first place + self.state + .ecs() + .fetch::<EventBus<common::event::ServerEvent>>() + .recv_all(); // 5) Terrain let pos = self @@ -1728,6 +1740,11 @@ impl Client { alias_of_uid(attacker_uid), alias_of_uid(victim) ), + KillSource::Player(attacker_uid, KillType::Buff) => format!( + "[{}] killed [{}]", + alias_of_uid(attacker_uid), + alias_of_uid(victim) + ), KillSource::NonPlayer(attacker_name, KillType::Melee) => { format!("{} killed [{}]", attacker_name, alias_of_uid(victim)) }, @@ -1742,6 +1759,9 @@ impl Client { attacker_name, alias_of_uid(victim) ), + KillSource::NonPlayer(attacker_name, KillType::Buff) => { + format!("{} killed [{}]", attacker_name, alias_of_uid(victim)) + }, KillSource::Environment(environment) => { format!("[{}] died in {}", alias_of_uid(victim), environment) }, @@ -1767,6 +1787,9 @@ impl Client { KillSource::Player(attacker_uid, KillType::Energy) => message .replace("{attacker}", &alias_of_uid(attacker_uid)) .replace("{victim}", &alias_of_uid(victim)), + KillSource::Player(attacker_uid, KillType::Buff) => message + .replace("{attacker}", &alias_of_uid(attacker_uid)) + .replace("{victim}", &alias_of_uid(victim)), KillSource::NonPlayer(attacker_name, KillType::Melee) => message .replace("{attacker}", attacker_name) .replace("{victim}", &alias_of_uid(victim)), @@ -1779,6 +1802,9 @@ impl Client { KillSource::NonPlayer(attacker_name, KillType::Energy) => message .replace("{attacker}", attacker_name) .replace("{victim}", &alias_of_uid(victim)), + KillSource::NonPlayer(attacker_name, KillType::Buff) => message + .replace("{attacker}", attacker_name) + .replace("{victim}", &alias_of_uid(victim)), KillSource::Environment(environment) => message .replace("{name}", &alias_of_uid(victim)) .replace("{environment}", environment), diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs new file mode 100644 index 0000000000..08a489baf3 --- /dev/null +++ b/common/src/comp/buff.rs @@ -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>>; +} diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index 4774b18683..0b748db9d4 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -51,6 +51,7 @@ pub enum KillType { Projectile, Explosion, Energy, + Buff, // Projectile(String), TODO: add projectile name when available } diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index b9c7e1cb4c..5c7f3558c8 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -1,4 +1,8 @@ -use crate::{comp::inventory::slot::Slot, sync::Uid, util::Dir}; +use crate::{ + comp::{inventory::slot::Slot, BuffKind}, + sync::Uid, + util::Dir, +}; use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; @@ -37,6 +41,7 @@ pub enum ControlEvent { Unmount, InventoryManip(InventoryManip), GroupManip(GroupManip), + RemoveBuff(BuffKind), Respawn, } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index c8204c6eed..329546a54e 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -3,6 +3,7 @@ mod admin; pub mod agent; pub mod beam; pub mod body; +pub mod buff; mod character_state; pub mod chat; mod controller; @@ -31,6 +32,10 @@ pub use body::{ biped_large, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, Body, BodyData, }; +pub use buff::{ + Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs, + ModifierKind, +}; pub use character_state::{Attacking, CharacterState, StateUpdate}; pub use chat::{ ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, diff --git a/common/src/comp/stats.rs b/common/src/comp/stats.rs index e1e129b6b3..7289bfbdf7 100644 --- a/common/src/comp/stats.rs +++ b/common/src/comp/stats.rs @@ -8,6 +8,7 @@ use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; use std::{error::Error, fmt}; +/// Specifies what and how much changed current health #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct HealthChange { pub amount: i32, @@ -20,6 +21,7 @@ pub enum HealthSource { Projectile { owner: Option<Uid> }, Explosion { owner: Option<Uid> }, Energy { owner: Option<Uid> }, + Buff { owner: Option<Uid> }, Suicide, World, Revive, @@ -32,6 +34,7 @@ pub enum HealthSource { #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct Health { + base_max: u32, current: u32, maximum: u32, pub last_change: (f64, HealthChange), @@ -67,11 +70,21 @@ impl Health { self.last_change = (0.0, change); } - // This is private because max hp is based on the level - fn set_maximum(&mut self, amount: u32) { + // This function changes the modified max health value, not the base health + // value. The modified health value takes into account buffs and other temporary + // changes to max health. + pub fn set_maximum(&mut self, amount: u32) { self.maximum = amount; self.current = self.current.min(self.maximum); } + + // This is private because max hp is based on the level + fn set_base_max(&mut self, amount: u32) { + self.base_max = amount; + self.current = self.current.min(self.maximum); + } + + pub fn reset_max(&mut self) { self.maximum = self.base_max; } } #[derive(Debug)] pub enum StatChangeError { @@ -148,6 +161,8 @@ impl Stats { // TODO: Delete this once stat points will be a thing pub fn update_max_hp(&mut self, body: Body) { + self.health + .set_base_max(body.base_health() + body.base_health_increase() * self.level.amount); self.health .set_maximum(body.base_health() + body.base_health_increase() * self.level.amount); } @@ -179,6 +194,7 @@ impl Stats { health: Health { current: 0, maximum: 0, + base_max: 0, last_change: (0.0, HealthChange { amount: 0, cause: HealthSource::Revive, @@ -198,6 +214,7 @@ impl Stats { }; stats.update_max_hp(body); + stats .health .set_to(stats.health.maximum(), HealthSource::Revive); @@ -213,6 +230,7 @@ impl Stats { health: Health { current: 0, maximum: 0, + base_max: 0, last_change: (0.0, HealthChange { amount: 0, cause: HealthSource::Revive, diff --git a/common/src/event.rs b/common/src/event.rs index 542650d8a4..a51620c961 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -106,6 +106,10 @@ pub enum ServerEvent { ChatCmd(EcsEntity, String), /// Send a chat message to the player from an npc or other player Chat(comp::UnresolvedChatMsg), + Buff { + entity: EcsEntity, + buff_change: comp::BuffChange, + }, } pub struct EventBus<E> { diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index ce9852720f..41ab1bda71 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -13,6 +13,7 @@ sum_type! { Player(comp::Player), CanBuild(comp::CanBuild), Stats(comp::Stats), + Buffs(comp::Buffs), Energy(comp::Energy), LightEmitter(comp::LightEmitter), Item(comp::Item), @@ -42,6 +43,7 @@ sum_type! { Player(PhantomData<comp::Player>), CanBuild(PhantomData<comp::CanBuild>), Stats(PhantomData<comp::Stats>), + Buffs(PhantomData<comp::Buffs>), Energy(PhantomData<comp::Energy>), LightEmitter(PhantomData<comp::LightEmitter>), Item(PhantomData<comp::Item>), @@ -71,6 +73,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Player(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), @@ -98,6 +101,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Player(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), @@ -125,6 +129,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Player(_) => sync::handle_remove::<comp::Player>(entity, world), EcsCompPhantom::CanBuild(_) => sync::handle_remove::<comp::CanBuild>(entity, world), EcsCompPhantom::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world), + EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world), EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world), EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::<comp::LightEmitter>(entity, world) diff --git a/common/src/state.rs b/common/src/state.rs index d54a5d9433..cbf00daaf2 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -112,6 +112,7 @@ impl State { ecs.register::<comp::Body>(); ecs.register::<comp::Player>(); ecs.register::<comp::Stats>(); + ecs.register::<comp::Buffs>(); ecs.register::<comp::Energy>(); ecs.register::<comp::CanBuild>(); ecs.register::<comp::LightEmitter>(); diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index a86e6ec529..06d39cb1c8 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -601,6 +601,7 @@ impl<'a> System<'a> for Sys { if let comp::HealthSource::Attack { by } | comp::HealthSource::Projectile { owner: Some(by) } | comp::HealthSource::Energy { owner: Some(by) } + | comp::HealthSource::Buff { owner: Some(by) } | comp::HealthSource::Explosion { owner: Some(by) } = my_stats.health.last_change.1.cause { diff --git a/common/src/sys/buff.rs b/common/src/sys/buff.rs new file mode 100644 index 0000000000..3cff89f8fc --- /dev/null +++ b/common/src/sys/buff.rs @@ -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); + } +} diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index a9327b96ef..cfa469a910 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, - Loadout, Ori, Pos, Scale, Stats, + buff, group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, + HealthSource, Loadout, Ori, Pos, Scale, Stats, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, @@ -9,7 +9,9 @@ use crate::{ sync::Uid, util::Dir, }; +use rand::{thread_rng, Rng}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; +use std::time::Duration; use vek::*; pub const BLOCK_EFFICIENCY: f32 = 0.9; @@ -150,6 +152,24 @@ impl<'a> System<'a> for Sys { cause, }, }); + + // Apply bleeding buff on melee hits with 10% chance + // TODO: Don't have buff uniformly applied on all melee attacks + if thread_rng().gen::<f32>() < 0.1 { + use buff::*; + server_emitter.emit(ServerEvent::Buff { + entity: b, + buff_change: BuffChange::Add(Buff::new( + BuffKind::Bleeding, + BuffData { + strength: attack.base_damage as f32 / 10.0, + duration: Some(Duration::from_secs(10)), + }, + vec![BuffCategory::Physical], + BuffSource::Character { by: *uid }, + )), + }); + } attack.hit_count += 1; } if attack.knockback != 0.0 && damage.healthchange != 0.0 { diff --git a/common/src/sys/controller.rs b/common/src/sys/controller.rs index 380b176adf..3fe6b8bedb 100644 --- a/common/src/sys/controller.rs +++ b/common/src/sys/controller.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ slot::{EquipSlot, Slot}, - CharacterState, ControlEvent, Controller, InventoryManip, + BuffChange, CharacterState, ControlEvent, Controller, InventoryManip, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, @@ -83,6 +83,12 @@ impl<'a> System<'a> for Sys { server_emitter.emit(ServerEvent::Mount(entity, mountee_entity)); } }, + ControlEvent::RemoveBuff(buff_id) => { + server_emitter.emit(ServerEvent::Buff { + entity, + buff_change: BuffChange::RemoveFromController(buff_id), + }); + }, ControlEvent::Unmount => server_emitter.emit(ServerEvent::Unmount(entity)), ControlEvent::EnableLantern => { server_emitter.emit(ServerEvent::EnableLantern(entity)) diff --git a/common/src/sys/mod.rs b/common/src/sys/mod.rs index 99b2e56047..98c70fd145 100644 --- a/common/src/sys/mod.rs +++ b/common/src/sys/mod.rs @@ -1,5 +1,6 @@ pub mod agent; mod beam; +mod buff; pub mod character_behavior; pub mod combat; pub mod controller; @@ -23,6 +24,7 @@ pub const PHYS_SYS: &str = "phys_sys"; pub const PROJECTILE_SYS: &str = "projectile_sys"; pub const SHOCKWAVE_SYS: &str = "shockwave_sys"; pub const STATS_SYS: &str = "stats_sys"; +pub const BUFFS_SYS: &str = "buffs_sys"; pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(agent::Sys, AGENT_SYS, &[]); @@ -32,6 +34,7 @@ pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { CONTROLLER_SYS, ]); dispatch_builder.add(stats::Sys, STATS_SYS, &[]); + dispatch_builder.add(buff::Sys, BUFFS_SYS, &[]); dispatch_builder.add(phys::Sys, PHYS_SYS, &[CONTROLLER_SYS, MOUNT_SYS, STATS_SYS]); dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]); dispatch_builder.add(shockwave::Sys, SHOCKWAVE_SYS, &[PHYS_SYS]); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d4daa1b3bc..e32e7318b7 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -6,7 +6,7 @@ use crate::{ use common::{ assets::Asset, comp::{ - self, + self, buff, chat::{KillSource, KillType}, object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource, Item, Player, Pos, Stats, @@ -165,11 +165,34 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc KillSource::NonPlayer("<?>".to_string(), KillType::Energy) } }, + HealthSource::Buff { owner: Some(by) } => { + // Get energy owner entity + if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { + // Check if attacker is another player or entity with stats (npc) + if state + .ecs() + .read_storage::<Player>() + .get(char_entity) + .is_some() + { + KillSource::Player(by, KillType::Buff) + } else if let Some(stats) = + state.ecs().read_storage::<Stats>().get(char_entity) + { + KillSource::NonPlayer(stats.name.clone(), KillType::Buff) + } else { + KillSource::NonPlayer("<?>".to_string(), KillType::Buff) + } + } else { + KillSource::NonPlayer("<?>".to_string(), KillType::Buff) + } + }, HealthSource::World => KillSource::FallDamage, HealthSource::Suicide => KillSource::Suicide, HealthSource::Projectile { owner: None } | HealthSource::Explosion { owner: None } | HealthSource::Energy { owner: None } + | HealthSource::Buff { owner: None } | HealthSource::Revive | HealthSource::Command | HealthSource::LevelUp @@ -189,6 +212,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc let by = if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } | HealthSource::Energy { owner: Some(by) } + | HealthSource::Buff { owner: Some(by) } | HealthSource::Explosion { owner: Some(by) } = cause { by @@ -674,3 +698,65 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) { PlayerListUpdate::LevelChange(*uid, new_level), )); } + +pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::BuffChange) { + let ecs = &server.state.ecs(); + let mut buffs_all = ecs.write_storage::<comp::Buffs>(); + if let Some(buffs) = buffs_all.get_mut(entity) { + use buff::BuffChange; + match buff_change { + BuffChange::Add(new_buff) => { + buffs.insert(new_buff); + }, + BuffChange::RemoveById(ids) => { + for id in ids { + buffs.remove(id); + } + }, + BuffChange::RemoveByKind(kind) => { + buffs.remove_kind(kind); + }, + BuffChange::RemoveFromController(kind) => { + if kind.is_buff() { + buffs.remove_kind(kind); + } + }, + BuffChange::RemoveByCategory { + all_required, + any_required, + none_required, + } => { + let mut ids_to_remove = Vec::new(); + for (id, buff) in buffs.buffs.iter() { + let mut required_met = true; + for required in &all_required { + if !buff.cat_ids.iter().any(|cat| cat == required) { + required_met = false; + break; + } + } + let mut any_met = any_required.is_empty(); + for any in &any_required { + if buff.cat_ids.iter().any(|cat| cat == any) { + any_met = true; + break; + } + } + let mut none_met = true; + for none in &none_required { + if buff.cat_ids.iter().any(|cat| cat == none) { + none_met = false; + break; + } + } + if required_met && any_met && none_met { + ids_to_remove.push(*id); + } + } + for id in ids_to_remove { + buffs.remove(id); + } + }, + } + } +} diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 9d64de3202..651b49a350 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -8,8 +8,8 @@ use entity_creation::{ handle_loaded_character_data, handle_shockwave, handle_shoot, }; use entity_manipulation::{ - handle_damage, handle_destroy, handle_explosion, handle_knockback, handle_land_on_ground, - handle_level_up, handle_respawn, + handle_buff, handle_damage, handle_destroy, handle_explosion, handle_knockback, + handle_land_on_ground, handle_level_up, handle_respawn, }; use group_manip::handle_group; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; @@ -133,6 +133,10 @@ impl Server { ServerEvent::Chat(msg) => { chat_messages.push(msg); }, + ServerEvent::Buff { + entity, + buff_change, + } => handle_buff(self, entity, buff_change), } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a4208f0934..06d7c523e3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -111,6 +111,7 @@ impl StateExt for State { .with(comp::Gravity(1.0)) .with(comp::CharacterState::default()) .with(loadout) + .with(comp::Buffs::default()) } fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { @@ -202,6 +203,7 @@ impl StateExt for State { entity, comp::Alignment::Owned(self.read_component_copied(entity).unwrap()), ); + self.write_component(entity, comp::Buffs::default()); // Make sure physics components are updated self.write_component(entity, comp::ForceUpdate); diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index fcecccb288..68d568b66c 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,7 +1,7 @@ use super::SysTimer; use common::{ comp::{ - BeamSegment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, + BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, Stats, Sticky, Vel, }, @@ -44,6 +44,7 @@ pub struct TrackedComps<'a> { pub body: ReadStorage<'a, Body>, pub player: ReadStorage<'a, Player>, pub stats: ReadStorage<'a, Stats>, + pub buffs: ReadStorage<'a, Buffs>, pub energy: ReadStorage<'a, Energy>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, @@ -85,6 +86,10 @@ impl<'a> TrackedComps<'a> { .get(entity) .cloned() .map(|c| comps.push(c.into())); + self.buffs + .get(entity) + .cloned() + .map(|c| comps.push(c.into())); self.energy .get(entity) .cloned() @@ -157,6 +162,7 @@ pub struct ReadTrackers<'a> { pub body: ReadExpect<'a, UpdateTracker<Body>>, pub player: ReadExpect<'a, UpdateTracker<Player>>, pub stats: ReadExpect<'a, UpdateTracker<Stats>>, + pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>, pub energy: ReadExpect<'a, UpdateTracker<Energy>>, pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>, pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>, @@ -187,6 +193,7 @@ impl<'a> ReadTrackers<'a> { .with_component(&comps.uid, &*self.body, &comps.body, filter) .with_component(&comps.uid, &*self.player, &comps.player, filter) .with_component(&comps.uid, &*self.stats, &comps.stats, filter) + .with_component(&comps.uid, &*self.buffs, &comps.buffs, filter) .with_component(&comps.uid, &*self.energy, &comps.energy, filter) .with_component(&comps.uid, &*self.can_build, &comps.can_build, filter) .with_component( @@ -224,6 +231,7 @@ pub struct WriteTrackers<'a> { body: WriteExpect<'a, UpdateTracker<Body>>, player: WriteExpect<'a, UpdateTracker<Player>>, stats: WriteExpect<'a, UpdateTracker<Stats>>, + buffs: WriteExpect<'a, UpdateTracker<Buffs>>, energy: WriteExpect<'a, UpdateTracker<Energy>>, can_build: WriteExpect<'a, UpdateTracker<CanBuild>>, light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>, @@ -248,6 +256,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.body.record_changes(&comps.body); trackers.player.record_changes(&comps.player); trackers.stats.record_changes(&comps.stats); + trackers.buffs.record_changes(&comps.buffs); trackers.energy.record_changes(&comps.energy); trackers.can_build.record_changes(&comps.can_build); trackers.light_emitter.record_changes(&comps.light_emitter); @@ -283,6 +292,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { }; log_counts!(uid, "Uids"); log_counts!(body, "Bodies"); + log_counts!(buffs, "Buffs"); log_counts!(player, "Players"); log_counts!(stats, "Stats"); log_counts!(energy, "Energies"); @@ -307,6 +317,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::<Body>(); world.register_tracker::<Player>(); world.register_tracker::<Stats>(); + world.register_tracker::<Buffs>(); world.register_tracker::<Energy>(); world.register_tracker::<CanBuild>(); world.register_tracker::<LightEmitter>(); diff --git a/voxygen/src/ecs/sys/floater.rs b/voxygen/src/ecs/sys/floater.rs index 6964010d9f..ad9c8c1de7 100644 --- a/voxygen/src/ecs/sys/floater.rs +++ b/voxygen/src/ecs/sys/floater.rs @@ -76,6 +76,7 @@ impl<'a> System<'a> for Sys { | HealthSource::Projectile { owner: Some(by) } | HealthSource::Energy { owner: Some(by) } | HealthSource::Explosion { owner: Some(by) } + | HealthSource::Buff { owner: Some(by) } | HealthSource::Healing { by: Some(by) } => { let by_me = my_uid.map_or(false, |&uid| by == uid); // If the attack was by me also reset this timer diff --git a/voxygen/src/hud/buffs.rs b/voxygen/src/hud/buffs.rs new file mode 100644 index 0000000000..1bd91eabc3 --- /dev/null +++ b/voxygen/src/hud/buffs.rs @@ -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 + } +} diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 8194ddf445..b9424d366a 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -373,6 +373,10 @@ impl<'a> Widget for Chat<'a> { .localized_strings .get("hud.chat.pvp_energy_kill_msg") .to_string(), + KillSource::Player(_, KillType::Buff) => self + .localized_strings + .get("hud.chat.pvp_buff_kill_msg") + .to_string(), KillSource::NonPlayer(_, KillType::Melee) => self .localized_strings .get("hud.chat.npc_melee_kill_msg") @@ -389,6 +393,10 @@ impl<'a> Widget for Chat<'a> { .localized_strings .get("hud.chat.npc_energy_kill_msg") .to_string(), + KillSource::NonPlayer(_, KillType::Buff) => self + .localized_strings + .get("hud.chat.npc_buff_kill_msg") + .to_string(), KillSource::Environment(_) => self .localized_strings .get("hud.chat.environmental_kill_msg") diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 26060b3084..1bfeaa1cb2 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,15 +1,20 @@ use super::{ - img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, - MANA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, + img_ids::{Imgs, ImgsRot}, + Show, BLACK, BUFF_COLOR, DEBUFF_COLOR, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, + LOW_HP_COLOR, STAMINA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::{ - i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, - window::GameInput, GlobalState, + hud::get_buff_info, + i18n::VoxygenLocalization, + settings::Settings, + ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, + window::GameInput, + GlobalState, }; use client::{self, Client}; use common::{ - comp::{group::Role, Stats}, + comp::{group::Role, BuffKind, Stats}, sync::{Uid, WorldSyncExt}, }; use conrod_core::{ @@ -19,7 +24,6 @@ use conrod_core::{ widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; use specs::{saveload::MarkerAllocator, WorldExt}; - widget_ids! { pub struct Ids { group_button, @@ -44,6 +48,8 @@ widget_ids! { member_panels_txt[], member_health[], member_stam[], + buffs[], + buff_timers[], dead_txt[], health_txt[], timeout_bg, @@ -63,10 +69,12 @@ pub struct Group<'a> { client: &'a Client, settings: &'a Settings, imgs: &'a Imgs, + rot_imgs: &'a ImgsRot, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc<VoxygenLocalization>, pulse: f32, global_state: &'a GlobalState, + tooltip_manager: &'a mut TooltipManager, #[conrod(common_builder)] common: widget::CommonBuilder, @@ -79,20 +87,24 @@ impl<'a> Group<'a> { client: &'a Client, settings: &'a Settings, imgs: &'a Imgs, + rot_imgs: &'a ImgsRot, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc<VoxygenLocalization>, pulse: f32, global_state: &'a GlobalState, + tooltip_manager: &'a mut TooltipManager, ) -> Self { Self { show, client, settings, imgs, + rot_imgs, fonts, localized_strings, pulse, global_state, + tooltip_manager, common: widget::CommonBuilder::default(), } } @@ -127,8 +139,26 @@ impl<'a> Widget for Group<'a> { #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; - let mut events = Vec::new(); + let localized_strings = self.localized_strings; + let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer + let buffs_tooltip = Tooltip::new({ + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }) + .title_font_size(self.fonts.cyri.scale(15)) + .parent(ui.window) + .desc_font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); // Don't show pets let group_members = self @@ -293,31 +323,35 @@ impl<'a> Widget for Group<'a> { let client_state = self.client.state(); let stats = client_state.ecs().read_storage::<common::comp::Stats>(); let energy = client_state.ecs().read_storage::<common::comp::Energy>(); + let buffs = client_state.ecs().read_storage::<common::comp::Buffs>(); let uid_allocator = client_state .ecs() .read_resource::<common::sync::UidAllocator>(); - + let offset = if self.global_state.settings.gameplay.toggle_debug { + 320.0 + } else { + 110.0 + }; + // Keep track of the total number of widget ids we are using for buffs + let mut total_buff_count = 0; for (i, &uid) in group_members.iter().copied().enumerate() { self.show.group = true; let entity = uid_allocator.retrieve_entity_internal(uid.into()); let stats = entity.and_then(|entity| stats.get(entity)); let energy = entity.and_then(|entity| energy.get(entity)); + let buffs = entity.and_then(|entity| buffs.get(entity)); + if let Some(stats) = stats { let char_name = stats.name.to_string(); let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64; // change panel positions when debug info is shown - let offset = if self.global_state.settings.gameplay.toggle_debug { - 290.0 - } else { - 110.0 - }; let back = if i == 0 { Image::new(self.imgs.member_bg) .top_left_with_margins_on(ui.window, offset, 20.0) } else { Image::new(self.imgs.member_bg) - .down_from(state.ids.member_panels_bg[i - 1], 40.0) + .down_from(state.ids.member_panels_bg[i - 1], 45.0) }; let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); @@ -386,66 +420,174 @@ impl<'a> Widget for Group<'a> { .set(state.ids.member_panels_frame[i], ui); // Panel Text Text::new(&char_name) - .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0) - .font_size(20) - .font_id(self.fonts.cyri.conrod_id) - .color(BLACK) - .w(300.0) // limit name length display - .set(state.ids.member_panels_txt_bg[i], ui); + .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(BLACK) + .w(300.0) // limit name length display + .set(state.ids.member_panels_txt_bg[i], ui); Text::new(&char_name) - .bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0) - .font_size(20) - .font_id(self.fonts.cyri.conrod_id) - .color(if is_leader { ERROR_COLOR } else { GROUP_COLOR }) - .w(300.0) // limit name length display - .set(state.ids.member_panels_txt[i], ui); + .bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(if is_leader { ERROR_COLOR } else { GROUP_COLOR }) + .w(300.0) // limit name length display + .set(state.ids.member_panels_txt[i], ui); if let Some(energy) = energy { let stam_perc = energy.current() as f64 / energy.maximum() as f64; // Stamina Image::new(self.imgs.bar_content) .w_h(100.0 * stam_perc, 8.0) - .color(Some(MANA_COLOR)) + .color(Some(STAMINA_COLOR)) .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0) .set(state.ids.member_stam[i], ui); } - } else { - // Values N.A. - if let Some(stats) = stats { + if let Some(buffs) = buffs { + // Limit displayed buffs to 11 + let buff_count = buffs.kinds.len().min(11); + total_buff_count += buff_count; + let gen = &mut ui.widget_id_generator(); + if state.ids.buffs.len() < total_buff_count { + state.update(|state| state.ids.buffs.resize(total_buff_count, gen)); + } + if state.ids.buff_timers.len() < total_buff_count { + state.update(|state| { + state.ids.buff_timers.resize(total_buff_count, gen) + }); + } + // Create Buff Widgets + let mut prev_id = None; + state + .ids + .buffs + .iter() + .copied() + .zip(state.ids.buff_timers.iter().copied()) + .skip(total_buff_count - buff_count) + .zip(buffs.iter_active().map(get_buff_info)) + .for_each(|((id, timer_id), buff)| { + let max_duration = buff.data.duration; + let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani); + let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0); + let current_duration = buff.dur; + let duration_percentage = current_duration.map_or(1000.0, |cur| { + max_duration.map_or(1000.0, |max| { + cur.as_secs_f32() / max.as_secs_f32() * 1000.0 + }) + }) as u32; // Percentage to determine which frame of the timer overlay is displayed + let buff_img = match buff.kind { + BuffKind::Regeneration { .. } => self.imgs.buff_plus_0, + BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0, + BuffKind::Cursed { .. } => self.imgs.debuff_skull_0, + }; + let buff_widget = Image::new(buff_img).w_h(15.0, 15.0); + let buff_widget = if let Some(id) = prev_id { + buff_widget.right_from(id, 1.0) + } else { + buff_widget.bottom_left_with_margins_on( + state.ids.member_panels_frame[i], + -16.0, + 1.0, + ) + }; + prev_id = Some(id); + buff_widget + .color( + if current_duration + .map_or(false, |cur| cur.as_secs_f32() < 10.0) + { + Some(pulsating_col) + } else { + Some(norm_col) + }, + ) + .set(id, ui); + // Create Buff tooltip + let title = match buff.kind { + BuffKind::Regeneration { .. } => { + localized_strings.get("buff.title.heal_test") + }, + BuffKind::Bleeding { .. } => { + localized_strings.get("debuff.title.bleed_test") + }, + _ => localized_strings.get("buff.title.missing"), + }; + let remaining_time = if current_duration.is_none() { + "Permanent".to_string() + } else { + format!( + "Remaining: {:.0}s", + current_duration.unwrap().as_secs_f32() + ) + }; + let desc_txt = match buff.kind { + BuffKind::Regeneration { .. } => { + localized_strings.get("buff.desc.heal_test") + }, + BuffKind::Bleeding { .. } => { + localized_strings.get("debuff.desc.bleed_test") + }, + _ => localized_strings.get("buff.desc.missing"), + }; + let desc = format!("{}\n\n{}", desc_txt, remaining_time); + Image::new(match duration_percentage as u64 { + 875..=1000 => self.imgs.nothing, // 8/8 + 750..=874 => self.imgs.buff_0, // 7/8 + 625..=749 => self.imgs.buff_1, // 6/8 + 500..=624 => self.imgs.buff_2, // 5/8 + 375..=499 => self.imgs.buff_3, // 4/8 + 250..=374 => self.imgs.buff_4, // 3/8 + 125..=249 => self.imgs.buff_5, // 2/8 + 0..=124 => self.imgs.buff_6, // 1/8 + _ => self.imgs.nothing, + }) + .w_h(15.0, 15.0) + .middle_of(id) + .with_tooltip( + self.tooltip_manager, + title, + &desc, + &buffs_tooltip, + if buff.is_buff { + BUFF_COLOR + } else { + DEBUFF_COLOR + }, + ) + .set(timer_id, ui); + }); + } else { + // Values N.A. Text::new(&stats.name.to_string()) .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) .color(GROUP_COLOR) .set(state.ids.member_panels_txt[i], ui); - }; - let offset = if self.global_state.settings.gameplay.toggle_debug { - 210.0 - } else { - 110.0 - }; - let back = if i == 0 { - Image::new(self.imgs.member_bg) - .top_left_with_margins_on(ui.window, offset, 20.0) - } else { - Image::new(self.imgs.member_bg) - .down_from(state.ids.member_panels_bg[i - 1], 40.0) - }; - back.w_h(152.0, 36.0) - .color(Some(TEXT_COLOR)) - .set(state.ids.member_panels_bg[i], ui); - // Panel Frame - Image::new(self.imgs.member_frame) - .w_h(152.0, 36.0) - .middle_of(state.ids.member_panels_bg[i]) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.member_panels_frame[i], ui); - // Panel Text - Text::new(&self.localized_strings.get("hud.group.out_of_range")) - .mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0) - .font_size(16) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.dead_txt[i], ui); + let back = if i == 0 { + Image::new(self.imgs.member_bg) + .top_left_with_margins_on(ui.window, offset, 20.0) + } else { + Image::new(self.imgs.member_bg) + .down_from(state.ids.member_panels_bg[i - 1], 40.0) + }; + back.w_h(152.0, 36.0) + .color(Some(TEXT_COLOR)) + .set(state.ids.member_panels_bg[i], ui); + // Panel Frame + Image::new(self.imgs.member_frame) + .w_h(152.0, 36.0) + .middle_of(state.ids.member_panels_bg[i]) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.member_panels_frame[i], ui); + // Panel Text + Text::new(&self.localized_strings.get("hud.group.out_of_range")) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0) + .font_size(16) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.dead_txt[i], ui); + } } } diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index eecd9d18da..fbb48033c0 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -147,22 +147,12 @@ image_ids! { // Skillbar level_up: "voxygen.element.misc_bg.level_up", - level_down: "voxygen.element.misc_bg.level_down", - xp_bar_mid: "voxygen.element.skillbar.xp_bar_mid", - xp_bar_left: "voxygen.element.skillbar.xp_bar_left", - xp_bar_right: "voxygen.element.skillbar.xp_bar_right", - healthbar_bg: "voxygen.element.skillbar.healthbar_bg", - energybar_bg: "voxygen.element.skillbar.energybar_bg", + level_down:"voxygen.element.misc_bg.level_down", bar_content: "voxygen.element.skillbar.bar_content", - skillbar_slot_big: "voxygen.element.skillbar.skillbar_slot_big", - skillbar_slot_big_bg: "voxygen.element.skillbar.skillbar_slot_big", - skillbar_slot_big_act: "voxygen.element.skillbar.skillbar_slot_big", - skillbar_slot: "voxygen.element.skillbar.skillbar_slot", - skillbar_slot_act: "voxygen.element.skillbar.skillbar_slot_active", - skillbar_slot_l: "voxygen.element.skillbar.skillbar_slot_l", - skillbar_slot_r: "voxygen.element.skillbar.skillbar_slot_r", - skillbar_slot_l_act: "voxygen.element.skillbar.skillbar_slot_l_active", - skillbar_slot_r_act: "voxygen.element.skillbar.skillbar_slot_r_active", + skillbar_bg: "voxygen.element.skillbar.bg", + skillbar_frame: "voxygen.element.skillbar.frame", + m1_ico: "voxygen.element.icons.m1", + m2_ico: "voxygen.element.icons.m2", // Other Icons/Art skull: "voxygen.element.icons.skull", @@ -282,6 +272,7 @@ image_ids! { hammerleap: "voxygen.element.icons.skill_hammerleap", skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash", skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst", + missing_icon: "voxygen.element.icons.missing_icon_grey", // Buttons button: "voxygen.element.buttons.button", @@ -359,6 +350,24 @@ image_ids! { chat_tell: "voxygen.element.icons.chat.tell", chat_world: "voxygen.element.icons.chat.world", + // Buffs + buff_plus_0: "voxygen.element.icons.de_buffs.buff_plus_0", + + // Debuffs + debuff_skull_0: "voxygen.element.icons.de_buffs.debuff_skull_0", + debuff_bleed_0: "voxygen.element.icons.de_buffs.debuff_bleed_0", + + // Animation Frames + // Buff Frame + buff_0: "voxygen.element.animation.buff_frame.1", + buff_1: "voxygen.element.animation.buff_frame.2", + buff_2: "voxygen.element.animation.buff_frame.3", + buff_3: "voxygen.element.animation.buff_frame.4", + buff_4: "voxygen.element.animation.buff_frame.5", + buff_5: "voxygen.element.animation.buff_frame.6", + buff_6: "voxygen.element.animation.buff_frame.7", + buff_7: "voxygen.element.animation.buff_frame.8", + <BlankGraphic> nothing: (), } diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 1bdeef1395..ca993285cb 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -105,7 +105,7 @@ impl<'a> Widget for MiniMap<'a> { fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; let zoom = state.zoom; - const SCALE: f64 = 1.5; + const SCALE: f64 = 1.5; // TODO Make this a setting if self.show.mini_map { Image::new(self.imgs.mmap_frame) .w_h(174.0 * SCALE, 190.0 * SCALE) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 0350b89dd1..907a04857f 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1,4 +1,5 @@ mod bag; +mod buffs; mod buttons; mod chat; mod crafting; @@ -24,6 +25,7 @@ pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState}; pub use settings_window::ScaleChange; use bag::Bag; +use buffs::BuffsBar; use buttons::Buttons; use chat::Chat; use chrono::NaiveTime; @@ -58,7 +60,10 @@ use client::Client; use common::{ assets::Asset, comp, - comp::item::{ItemDesc, Quality}, + comp::{ + item::{ItemDesc, Quality}, + BuffKind, + }, span, sync::Uid, terrain::TerrainChunk, @@ -91,10 +96,12 @@ const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0); const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0); const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); -const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9); +const STAMINA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9); //const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0); //const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0); //const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0); +const BUFF_COLOR: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); +const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); // Item Quality Colors const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors @@ -239,6 +246,7 @@ widget_ids! { spell, skillbar, buttons, + buffs, esc_menu, small_window, social_window, @@ -264,6 +272,14 @@ widget_ids! { } } +#[derive(Clone, Copy)] +pub struct BuffInfo { + kind: comp::BuffKind, + data: comp::BuffData, + is_buff: bool, + dur: Option<Duration>, +} + pub struct DebugInfo { pub tps: f64, pub frame_time: Duration, @@ -315,6 +331,7 @@ pub enum Event { ChatTransp(f32), ChatCharName(bool), CrosshairType(CrosshairType), + BuffPosition(BuffPosition), ToggleXpBar(XpBar), Intro(Intro), ToggleBarNumbers(BarNumbers), @@ -348,6 +365,7 @@ pub enum Event { KickMember(common::sync::Uid), LeaveGroup, AssignLeader(common::sync::Uid), + RemoveBuff(BuffKind), } // TODO: Are these the possible layouts we want? @@ -388,6 +406,13 @@ pub enum ShortcutNumbers { On, Off, } + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum BuffPosition { + Bar, + Map, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum PressBehavior { Toggle = 0, @@ -722,6 +747,7 @@ impl Hud { let ecs = client.state().ecs(); let pos = ecs.read_storage::<comp::Pos>(); let stats = ecs.read_storage::<comp::Stats>(); + let buffs = ecs.read_storage::<comp::Buffs>(); let energy = ecs.read_storage::<comp::Energy>(); let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>(); let uids = ecs.read_storage::<common::sync::Uid>(); @@ -1120,11 +1146,12 @@ impl Hud { let speech_bubbles = &self.speech_bubbles; // Render overhead name tags and health bars - for (pos, info, bubble, stats, height_offset, hpfl, in_group) in ( + for (pos, info, bubble, stats, _, height_offset, hpfl, in_group) in ( &entities, &pos, interpolated.maybe(), &stats, + &buffs, energy.maybe(), scales.maybe(), &bodies, @@ -1138,7 +1165,7 @@ impl Hud { entity != me && !stats.is_dead }) .filter_map( - |(entity, pos, interpolated, stats, energy, scale, body, hpfl, uid)| { + |(entity, pos, interpolated, stats, buffs, energy, scale, body, hpfl, uid)| { // Use interpolated position if available let pos = interpolated.map_or(pos.0, |i| i.pos); let in_group = client.group_members().contains_key(uid); @@ -1168,6 +1195,7 @@ impl Hud { let info = display_overhead_info.then(|| overhead::Info { name: &stats.name, stats, + buffs, energy, }); let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) { @@ -1182,6 +1210,7 @@ impl Hud { info, bubble, stats, + buffs, body.height() * scale.map_or(1.0, |s| s.0) + 0.5, hpfl, in_group, @@ -1730,6 +1759,7 @@ impl Hud { // Bag button and nearby icons let ecs = client.state().ecs(); let stats = ecs.read_storage::<comp::Stats>(); + let buffs = ecs.read_storage::<comp::Buffs>(); if let Some(player_stats) = stats.get(client.entity()) { match Buttons::new( client, @@ -1754,6 +1784,48 @@ impl Hud { } } + // Buffs and Debuffs + if let Some(player_buffs) = buffs.get(client.entity()) { + for event in BuffsBar::new( + &self.imgs, + &self.fonts, + &self.rot_imgs, + tooltip_manager, + &self.voxygen_i18n, + &player_buffs, + self.pulse, + &global_state, + ) + .set(self.ids.buffs, ui_widgets) + { + match event { + buffs::Event::RemoveBuff(buff_id) => events.push(Event::RemoveBuff(buff_id)), + } + } + } + // Group Window + for event in Group::new( + &mut self.show, + client, + &global_state.settings, + &self.imgs, + &self.rot_imgs, + &self.fonts, + &self.voxygen_i18n, + self.pulse, + &global_state, + tooltip_manager, + ) + .set(self.ids.group_window, ui_widgets) + { + match event { + group::Event::Accept => events.push(Event::AcceptInvite), + group::Event::Decline => events.push(Event::DeclineInvite), + group::Event::Kick(uid) => events.push(Event::KickMember(uid)), + group::Event::LeaveGroup => events.push(Event::LeaveGroup), + group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), + } + } // Popup (waypoint saved and similar notifications) Popup::new( &self.voxygen_i18n, @@ -1828,8 +1900,8 @@ impl Hud { Some(stats), Some(loadout), Some(energy), - Some(character_state), - Some(controller), + Some(_character_state), + Some(_controller), Some(inventory), ) = ( stats.get(entity), @@ -1848,9 +1920,9 @@ impl Hud { &stats, &loadout, &energy, - &character_state, + //&character_state, self.pulse, - &controller, + //&controller, &inventory, &self.hotbar, tooltip_manager, @@ -1996,6 +2068,9 @@ impl Hud { settings_window::Event::ToggleZoomInvert(zoom_inverted) => { events.push(Event::ToggleZoomInvert(zoom_inverted)); }, + settings_window::Event::BuffPosition(buff_position) => { + events.push(Event::BuffPosition(buff_position)); + }, settings_window::Event::ToggleMouseYInvert(mouse_y_inverted) => { events.push(Event::ToggleMouseYInvert(mouse_y_inverted)); }, @@ -2032,9 +2107,6 @@ impl Hud { settings_window::Event::CrosshairType(crosshair_type) => { events.push(Event::CrosshairType(crosshair_type)); }, - settings_window::Event::ToggleXpBar(xp_bar) => { - events.push(Event::ToggleXpBar(xp_bar)); - }, settings_window::Event::ToggleBarNumbers(bar_numbers) => { events.push(Event::ToggleBarNumbers(bar_numbers)); }, @@ -2123,27 +2195,6 @@ impl Hud { } } } - // Group Window - for event in Group::new( - &mut self.show, - client, - &global_state.settings, - &self.imgs, - &self.fonts, - &self.voxygen_i18n, - self.pulse, - &global_state, - ) - .set(self.ids.group_window, ui_widgets) - { - match event { - group::Event::Accept => events.push(Event::AcceptInvite), - group::Event::Decline => events.push(Event::DeclineInvite), - group::Event::Kick(uid) => events.push(Event::KickMember(uid)), - group::Event::LeaveGroup => events.push(Event::LeaveGroup), - group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), - } - } // Spellbook if self.show.spell { @@ -2675,3 +2726,12 @@ pub fn get_quality_col<I: ItemDesc>(item: &I) -> Color { Quality::Debug => QUALITY_DEBUG, } } +// Get info about applied buffs +fn get_buff_info(buff: &comp::Buff) -> BuffInfo { + BuffInfo { + kind: buff.kind, + data: buff.data, + is_buff: buff.kind.is_buff(), + dur: buff.time, + } +} diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index a69bccbea6..7ebfb29cfc 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -1,14 +1,16 @@ use super::{ img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR, - MANA_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR, + REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR, }; use crate::{ + hud::get_buff_info, i18n::VoxygenLocalization, settings::GameplaySettings, ui::{fonts::ConrodVoxygenFonts, Ingameable}, }; -use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats}; +use common::comp::{BuffKind, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats}; use conrod_core::{ + color, position::Align, widget::{self, Image, Rectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, @@ -44,6 +46,11 @@ widget_ids! { health_txt, mana_bar, health_bar_fg, + + // Buffs + buffs_align, + buffs[], + buff_timers[], } } @@ -51,6 +58,7 @@ widget_ids! { pub struct Info<'a> { pub name: &'a str, pub stats: &'a Stats, + pub buffs: &'a Buffs, pub energy: Option<&'a Energy>, } @@ -119,17 +127,25 @@ impl<'a> Ingameable for Overhead<'a> { // - 1 for HP text // - If there's mana // - 1 Rect::new for mana - // + // If there are Buffs + // - 1 Alignment Rectangle + // - 10 + 10 Buffs and Timer Overlays (only if there is no speech bubble) // If there's a speech bubble // - 2 Text::new for speech bubble // - 1 Image::new for icon // - 10 Image::new for speech bubble (9-slice + tail) self.info.map_or(0, |info| { - 2 + if show_healthbar(info.stats) { - 5 + if info.energy.is_some() { 1 } else { 0 } - } else { - 0 - } + 2 + 1 + + if self.bubble.is_none() { + info.buffs.kinds.len().min(10) * 2 + } else { + 0 + } + + if show_healthbar(info.stats) { + 5 + if info.energy.is_some() { 1 } else { 0 } + } else { + 0 + } }) + if self.bubble.is_some() { 13 } else { 0 } } } @@ -155,6 +171,7 @@ impl<'a> Widget for Overhead<'a> { if let Some(Info { name, stats, + buffs, energy, }) = self.info { @@ -185,6 +202,84 @@ impl<'a> Widget for Overhead<'a> { 1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)), _ => format!("{:.0}M", (health_max as f64 / 1.0e6).max(1.0)), }; + // Buffs + // Alignment + let buff_count = buffs.kinds.len().min(11); + Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT) + .x_y(-1.0, name_y + 60.0) + .parent(id) + .set(state.ids.buffs_align, ui); + + let gen = &mut ui.widget_id_generator(); + if state.ids.buffs.len() < buff_count { + state.update(|state| state.ids.buffs.resize(buff_count, gen)); + }; + if state.ids.buff_timers.len() < buff_count { + state.update(|state| state.ids.buff_timers.resize(buff_count, gen)); + }; + + let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; //Animation timer + let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani); + let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0); + // Create Buff Widgets + if self.bubble.is_none() { + state + .ids + .buffs + .iter() + .copied() + .zip(state.ids.buff_timers.iter().copied()) + .zip(buffs.iter_active().map(get_buff_info)) + .enumerate() + .for_each(|(i, ((id, timer_id), buff))| { + // Limit displayed buffs + let max_duration = buff.data.duration; + let current_duration = buff.dur; + let duration_percentage = current_duration.map_or(1000.0, |cur| { + max_duration.map_or(1000.0, |max| { + cur.as_secs_f32() / max.as_secs_f32() * 1000.0 + }) + }) as u32; // Percentage to determine which frame of the timer overlay is displayed + let buff_img = match buff.kind { + BuffKind::Regeneration { .. } => self.imgs.buff_plus_0, + BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0, + BuffKind::Cursed { .. } => self.imgs.debuff_skull_0, + }; + let buff_widget = Image::new(buff_img).w_h(20.0, 20.0); + // Sort buffs into rows of 5 slots + let x = i % 5; + let y = i / 5; + let buff_widget = buff_widget.bottom_left_with_margins_on( + state.ids.buffs_align, + 0.0 + y as f64 * (21.0), + 0.0 + x as f64 * (21.0), + ); + buff_widget + .color( + if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) { + Some(pulsating_col) + } else { + Some(norm_col) + }, + ) + .set(id, ui); + + Image::new(match duration_percentage as u64 { + 875..=1000 => self.imgs.nothing, // 8/8 + 750..=874 => self.imgs.buff_0, // 7/8 + 625..=749 => self.imgs.buff_1, // 6/8 + 500..=624 => self.imgs.buff_2, // 5/8 + 375..=499 => self.imgs.buff_3, // 4/8 + 250..=374 => self.imgs.buff_4, // 3/8 + 125..=249 => self.imgs.buff_5, // 2/8 + 0..=124 => self.imgs.buff_6, // 1/8 + _ => self.imgs.nothing, + }) + .w_h(20.0, 20.0) + .middle_of(id) + .set(timer_id, ui); + }); + } // Name Text::new(name) .font_id(self.fonts.cyri.conrod_id) @@ -254,7 +349,7 @@ impl<'a> Widget for Overhead<'a> { Rectangle::fill_with( [72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT], - MANA_COLOR, + STAMINA_COLOR, ) .x_y( ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 6fb2473742..59f5a6f14b 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -1,9 +1,10 @@ use super::{ - img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar, - CRITICAL_HP_COLOR, ERROR_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, MENU_BG, + img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, + CRITICAL_HP_COLOR, ERROR_COLOR, HP_COLOR, LOW_HP_COLOR, MENU_BG, STAMINA_COLOR, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::{ + hud::BuffPosition, i18n::{list_localizations, LanguageMetadata, VoxygenLocalization}, render::{AaMode, CloudMode, FluidMode, LightingMode, RenderMode, ShadowMapMode, ShadowMode}, ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton}, @@ -19,7 +20,6 @@ use conrod_core::{ }; use core::convert::TryFrom; -use inline_tweak::*; use itertools::Itertools; use std::iter::once; use winit::monitor::VideoMode; @@ -159,6 +159,7 @@ widget_ids! { sfx_volume_text, audio_device_list, audio_device_text, + // hotbar_title, bar_numbers_title, show_bar_numbers_none_button, @@ -167,18 +168,20 @@ widget_ids! { show_bar_numbers_values_text, show_bar_numbers_percentage_button, show_bar_numbers_percentage_text, + // show_shortcuts_button, show_shortcuts_text, - show_xpbar_button, - show_xpbar_text, - show_bars_button, - show_bars_text, - placeholder, + buff_pos_bar_button, + buff_pos_bar_text, + buff_pos_map_button, + buff_pos_map_text, + // chat_transp_title, chat_transp_text, chat_transp_slider, chat_char_name_text, chat_char_name_button, + // sct_title, sct_show_text, sct_show_radio, @@ -195,6 +198,7 @@ widget_ids! { sct_num_dur_text, sct_num_dur_slider, sct_num_dur_value, + // speech_bubble_text, speech_bubble_dark_mode_text, speech_bubble_dark_mode_button, @@ -259,9 +263,9 @@ pub enum Event { ToggleHelp, ToggleDebug, ToggleTips(bool), - ToggleXpBar(XpBar), ToggleBarNumbers(BarNumbers), ToggleShortcutNumbers(ShortcutNumbers), + BuffPosition(BuffPosition), ChangeTab(SettingsTab), Close, AdjustMousePan(u32), @@ -796,40 +800,6 @@ impl<'a> Widget for SettingsWindow<'a> { .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.hotbar_title, ui); - // Show xp bar - if Button::image(match self.global_state.settings.gameplay.xp_bar { - XpBar::Always => self.imgs.checkbox_checked, - XpBar::OnGain => self.imgs.checkbox, - }) - .w_h(18.0, 18.0) - .hover_image(match self.global_state.settings.gameplay.xp_bar { - XpBar::Always => self.imgs.checkbox_checked_mo, - XpBar::OnGain => self.imgs.checkbox_mo, - }) - .press_image(match self.global_state.settings.gameplay.xp_bar { - XpBar::Always => self.imgs.checkbox_checked, - XpBar::OnGain => self.imgs.checkbox_press, - }) - .down_from(state.ids.hotbar_title, 8.0) - .set(state.ids.show_xpbar_button, ui) - .was_clicked() - { - match self.global_state.settings.gameplay.xp_bar { - XpBar::Always => events.push(Event::ToggleXpBar(XpBar::OnGain)), - XpBar::OnGain => events.push(Event::ToggleXpBar(XpBar::Always)), - } - } - Text::new( - &self - .localized_strings - .get("hud.settings.toggle_bar_experience"), - ) - .right_from(state.ids.show_xpbar_button, 10.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .graphics_for(state.ids.show_xpbar_button) - .color(TEXT_COLOR) - .set(state.ids.show_xpbar_text, ui); // Show Shortcut Numbers if Button::image(match self.global_state.settings.gameplay.shortcut_numbers { ShortcutNumbers::On => self.imgs.checkbox_checked, @@ -844,7 +814,7 @@ impl<'a> Widget for SettingsWindow<'a> { ShortcutNumbers::On => self.imgs.checkbox_checked, ShortcutNumbers::Off => self.imgs.checkbox_press, }) - .down_from(state.ids.show_xpbar_button, 8.0) + .down_from(state.ids.hotbar_title, 8.0) .set(state.ids.show_shortcuts_button, ui) .was_clicked() { @@ -864,11 +834,61 @@ impl<'a> Widget for SettingsWindow<'a> { .graphics_for(state.ids.show_shortcuts_button) .color(TEXT_COLOR) .set(state.ids.show_shortcuts_text, ui); - - Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT) - .down_from(state.ids.show_shortcuts_text, 30.0) - .set(state.ids.placeholder, ui); - + // Buff Position + // Buffs above skills + if Button::image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Bar => self.imgs.checkbox_checked, + BuffPosition::Map => self.imgs.checkbox, + }) + .w_h(18.0, 18.0) + .hover_image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Bar => self.imgs.checkbox_checked_mo, + BuffPosition::Map => self.imgs.checkbox_mo, + }) + .press_image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Bar => self.imgs.checkbox_checked, + BuffPosition::Map => self.imgs.checkbox_press, + }) + .down_from(state.ids.show_shortcuts_button, 8.0) + .set(state.ids.buff_pos_bar_button, ui) + .was_clicked() + { + events.push(Event::BuffPosition(BuffPosition::Bar)) + } + Text::new(&self.localized_strings.get("hud.settings.buffs_skillbar")) + .right_from(state.ids.buff_pos_bar_button, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .graphics_for(state.ids.show_shortcuts_button) + .color(TEXT_COLOR) + .set(state.ids.buff_pos_bar_text, ui); + // Buffs left from minimap + if Button::image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Map => self.imgs.checkbox_checked, + BuffPosition::Bar => self.imgs.checkbox, + }) + .w_h(18.0, 18.0) + .hover_image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Map => self.imgs.checkbox_checked_mo, + BuffPosition::Bar => self.imgs.checkbox_mo, + }) + .press_image(match self.global_state.settings.gameplay.buff_position { + BuffPosition::Map => self.imgs.checkbox_checked, + BuffPosition::Bar => self.imgs.checkbox_press, + }) + .down_from(state.ids.buff_pos_bar_button, 8.0) + .set(state.ids.buff_pos_map_button, ui) + .was_clicked() + { + events.push(Event::BuffPosition(BuffPosition::Map)) + } + Text::new(&self.localized_strings.get("hud.settings.buffs_mmap")) + .right_from(state.ids.buff_pos_map_button, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .graphics_for(state.ids.show_shortcuts_button) + .color(TEXT_COLOR) + .set(state.ids.buff_pos_map_text, ui); // Content Right Side /*Scrolling Combat text @@ -1692,7 +1712,7 @@ impl<'a> Widget for SettingsWindow<'a> { 0..=14 => CRITICAL_HP_COLOR, 15..=29 => LOW_HP_COLOR, 30..=50 => HP_COLOR, - _ => MANA_COLOR, + _ => STAMINA_COLOR, }; Text::new(&format!("FPS: {:.0}", self.fps)) .color(fps_col) @@ -2688,8 +2708,8 @@ impl<'a> Widget for SettingsWindow<'a> { }); }; for (i, language) in language_list.iter().enumerate() { - let button_w = tweak!(400.0); - let button_h = tweak!(50.0); + let button_w = 400.0; + let button_h = 50.0; let button = Button::image(if selected_language == &language.language_identifier { self.imgs.selection } else { @@ -2706,7 +2726,7 @@ impl<'a> Widget for SettingsWindow<'a> { .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press) .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(tweak!(22))) + .label_font_size(self.fonts.cyri.scale(22)) .label_font_id(self.fonts.cyri.conrod_id) .label_y(conrod_core::position::Relative::Scalar(2.0)) .set(state.ids.language_list[i], ui) diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 21e1aa2bd4..172671622e 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -2,8 +2,8 @@ use super::{ hotbar, img_ids::{Imgs, ImgsRot}, item_imgs::ItemImgs, - slots, BarNumbers, ShortcutNumbers, Show, XpBar, BLACK, CRITICAL_HP_COLOR, HP_COLOR, - LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, XP_COLOR, + slots, BarNumbers, ShortcutNumbers, Show, BLACK, CRITICAL_HP_COLOR, HP_COLOR, LOW_HP_COLOR, + STAMINA_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, XP_COLOR, }; use crate::{ i18n::VoxygenLocalization, @@ -20,7 +20,7 @@ use common::comp::{ tool::{Tool, ToolKind}, Hands, ItemKind, }, - CharacterState, ControllerInputs, Energy, Inventory, Loadout, Stats, + Energy, Inventory, Loadout, Stats, }; use conrod_core::{ color, @@ -32,22 +32,45 @@ use vek::*; widget_ids! { struct Ids { + // Death message death_message_1, death_message_2, death_message_1_bg, death_message_2_bg, - level_text, - next_level_text, - xp_bar_mid, - xp_bar_mid_top, - xp_bar_left, - xp_bar_left_top, - xp_bar_right, - xp_bar_right_top, - xp_bar_filling, - xp_bar_filling_top, - hotbar_align, - xp_bar_subdivision, + death_bg, + // Level up message + level_up, + level_down, + level_align, + level_message, + level_message_bg, + // Hurt BG + hurt_bg, + // Skillbar + alignment, + bg, + frame, + m1_ico, + m2_ico, + // Level + level_bg, + level, + // Exp-Bar + exp_alignment, + exp_filling, + // HP-Bar + hp_alignment, + hp_filling, + hp_txt_alignment, + hp_txt_bg, + hp_txt, + // Stamina-Bar + stamina_alignment, + stamina_filling, + stamina_txt_alignment, + stamina_txt_bg, + stamina_txt, + // Slots m1_slot, m1_slot_bg, m1_text, @@ -63,7 +86,6 @@ widget_ids! { slot1, slot1_text, slot1_text_bg, - //slot1_act, slot2, slot2_text, slot2_text_bg, @@ -91,29 +113,9 @@ widget_ids! { slot10, slot10_text, slot10_text_bg, - healthbar_bg, - healthbar_filling, - health_text, - health_text_bg, - energybar_bg, - energybar_filling, - energy_text, - energy_text_bg, - level_up, - level_down, - level_align, - level_message, - level_message_bg, - death_bg, - hurt_bg, } } -pub enum ResourceType { - Mana, - /*Rage, - *Focus, */ -} #[derive(WidgetCommon)] pub struct Skillbar<'a> { global_state: &'a GlobalState, @@ -124,8 +126,8 @@ pub struct Skillbar<'a> { stats: &'a Stats, loadout: &'a Loadout, energy: &'a Energy, - character_state: &'a CharacterState, - controller: &'a ControllerInputs, + // character_state: &'a CharacterState, + // controller: &'a ControllerInputs, inventory: &'a Inventory, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, @@ -134,7 +136,6 @@ pub struct Skillbar<'a> { pulse: f32, #[conrod(common_builder)] common: widget::CommonBuilder, - current_resource: ResourceType, show: &'a Show, } @@ -149,9 +150,9 @@ impl<'a> Skillbar<'a> { stats: &'a Stats, loadout: &'a Loadout, energy: &'a Energy, - character_state: &'a CharacterState, + // character_state: &'a CharacterState, pulse: f32, - controller: &'a ControllerInputs, + // controller: &'a ControllerInputs, inventory: &'a Inventory, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, @@ -168,11 +169,10 @@ impl<'a> Skillbar<'a> { stats, loadout, energy, - current_resource: ResourceType::Mana, common: widget::CommonBuilder::default(), - character_state, + // character_state, pulse, - controller, + // controller, inventory, hotbar, tooltip_manager, @@ -185,10 +185,7 @@ impl<'a> Skillbar<'a> { pub struct State { ids: Ids, - - last_xp_value: u32, last_level: u32, - last_update_xp: Instant, last_update_level: Instant, } @@ -200,10 +197,7 @@ impl<'a> Widget for Skillbar<'a> { fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { State { ids: Ids::new(id_gen), - - last_xp_value: 0, last_level: 1, - last_update_xp: Instant::now(), last_update_level: Instant::now(), } } @@ -214,8 +208,11 @@ impl<'a> Widget for Skillbar<'a> { fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; - let level = (self.stats.level.level()).to_string(); - let next_level = (self.stats.level.level() + 1).to_string(); + let level = if self.stats.level.level() > 999 { + "A".to_string() + } else { + (self.stats.level.level()).to_string() + }; let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64); @@ -227,12 +224,10 @@ impl<'a> Widget for Skillbar<'a> { hp_percentage = 0.0; energy_percentage = 0.0; }; - let scale = 2.0; let bar_values = self.global_state.settings.gameplay.bar_numbers; let shortcuts = self.global_state.settings.gameplay.shortcut_numbers; - const BG_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 0.99); 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); @@ -339,410 +334,147 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.death_message_2, ui); } } - // Experience-Bar - match self.global_state.settings.gameplay.xp_bar { - XpBar::Always => { - // Constant Display of the Exp Bar at the bottom of the screen - Image::new(self.imgs.xp_bar_mid) - .w_h(80.0 * scale, 10.0 * scale) - .mid_bottom_with_margin_on(ui.window, 2.0) - .set(state.ids.xp_bar_mid, ui); - Image::new(self.imgs.xp_bar_right) - .w_h(100.0 * scale, 10.0 * scale) - .right_from(state.ids.xp_bar_mid, 0.0) - .set(state.ids.xp_bar_right, ui); - Image::new(self.imgs.xp_bar_left) - .w_h(100.0 * scale, 10.0 * scale) - .left_from(state.ids.xp_bar_mid, 0.0) - .set(state.ids.xp_bar_left, ui); - Image::new(self.imgs.bar_content) - .w_h(260.0 * scale * exp_percentage, 5.0 * scale) - .color(Some(XP_COLOR)) - .top_left_with_margins_on(state.ids.xp_bar_left, 2.0 * scale, 10.0 * scale) - .set(state.ids.xp_bar_filling, ui); - // Level Display - if self.stats.level.level() < 10 { - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left, - 3.5 * scale, - 4.0 * scale, - ) - .font_size(self.fonts.cyri.scale(10)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right, - 3.5 * scale, - 4.0 * scale, - ) - .font_size(self.fonts.cyri.scale(10)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.next_level_text, ui); - } else if self.stats.level.level() < 100 { - // Change offset and fontsize for levels > 9 - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left, - 3.5 * scale, - 3.0 * scale, - ) - .font_size(self.fonts.cyri.scale(9)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right, - 3.5 * scale, - 3.0 * scale, - ) - .font_size(self.fonts.cyri.scale(9)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.next_level_text, ui); - } else { - // Change offset and fontsize for levels > 9 - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left, - 3.5 * scale, - 2.5 * scale, - ) - .font_size(self.fonts.cyri.scale(8)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right, - 3.5 * scale, - 2.5 * scale, - ) - .font_size(self.fonts.cyri.scale(8)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 1.0)) - .set(state.ids.next_level_text, ui); - } - // M1 Slot - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .top_left_with_margins_on(state.ids.xp_bar_mid, -40.0 * scale, 0.0) - .set(state.ids.m1_slot, ui); - }, - XpBar::OnGain => { - // Displays the Exp Bar at the top of the screen when exp is gained and fades it - // out afterwards - const FADE_IN_XP: f32 = 1.0; - const FADE_HOLD_XP: f32 = 3.0; - const FADE_OUT_XP: f32 = 2.0; - let current_xp = self.stats.exp.current(); - // Check if no other popup is displayed and a new one is needed - if state.last_update_xp.elapsed() - > Duration::from_secs_f32(FADE_IN_XP + FADE_HOLD_XP + FADE_OUT_XP) - && state.last_xp_value != current_xp - { - // Update last_value - state.update(|s| s.last_xp_value = current_xp); - state.update(|s| s.last_update_xp = Instant::now()); - } - - let seconds_xp = state.last_update_xp.elapsed().as_secs_f32(); - let fade_xp = if current_xp == 0 { - 0.0 - } else if seconds_xp < FADE_IN_XP { - seconds_xp / FADE_IN_XP - } else if seconds_xp < FADE_IN_XP + FADE_HOLD_XP { - 1.0 - } else { - (1.0 - (seconds_xp - FADE_IN_XP - FADE_HOLD_XP) / FADE_OUT_XP).max(0.0) - }; - // Hotbar parts - Image::new(self.imgs.xp_bar_mid) - .w_h(80.0 * scale * 1.5, 10.0 * scale * 1.5) - .mid_top_with_margin_on(ui.window, 20.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade_xp))) - .set(state.ids.xp_bar_mid_top, ui); - Image::new(self.imgs.xp_bar_right) - .w_h(100.0 * scale * 1.5, 10.0 * scale * 1.5) - .right_from(state.ids.xp_bar_mid_top, 0.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade_xp))) - .set(state.ids.xp_bar_right_top, ui); - Image::new(self.imgs.xp_bar_left) - .w_h(100.0 * scale * 1.5, 10.0 * scale * 1.5) - .left_from(state.ids.xp_bar_mid_top, 0.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade_xp))) - .set(state.ids.xp_bar_left_top, ui); - Image::new(self.imgs.bar_content) - .w_h(260.0 * scale * 1.5 * exp_percentage, 6.0 * scale * 1.5) - .color(Some(Color::Rgba(0.59, 0.41, 0.67, fade_xp))) - .top_left_with_margins_on( - state.ids.xp_bar_left_top, - 2.0 * scale * 1.5, - 10.0 * scale * 1.5, - ) - .set(state.ids.xp_bar_filling_top, ui); - // Level Display - if self.stats.level.level() < 10 { - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left_top, - 3.0 * scale * 1.5, - 4.0 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(17)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right_top, - 3.0 * scale * 1.5, - 4.0 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.next_level_text, ui); - } else if self.stats.level.level() < 100 { - // Change offset and fontsize for levels > 9 - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left_top, - 3.0 * scale * 1.5, - 3.0 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right_top, - 3.0 * scale * 1.5, - 3.0 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.next_level_text, ui); - } else { - // Change offset and fontsize for levels > 9 - Text::new(&level) - .bottom_left_with_margins_on( - state.ids.xp_bar_left_top, - 3.0 * scale * 1.5, - 2.75 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(12)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.level_text, ui); - Text::new(&next_level) - .bottom_right_with_margins_on( - state.ids.xp_bar_right_top, - 3.0 * scale * 1.5, - 2.75 * scale * 1.5, - ) - .font_size(self.fonts.cyri.scale(12)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade_xp)) - .set(state.ids.next_level_text, ui); - } - // Alignment for hotbar - Rectangle::fill_with([80.0 * scale, 1.0], color::TRANSPARENT) - .mid_bottom_with_margin_on(ui.window, 9.0) - .set(state.ids.hotbar_align, ui); - // M1 Slot - - match self.character_state { - CharacterState::BasicMelee { .. } => { - if self.controller.primary.is_pressed() { - let fade_pulse = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.6; //Animation timer; - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .top_left_with_margins_on( - state.ids.hotbar_align, - -40.0 * scale, - 0.0, - ) - .set(state.ids.m1_slot, ui); - Image::new(self.imgs.skillbar_slot_big_act) - .w_h(40.0 * scale, 40.0 * scale) - .middle_of(state.ids.m1_slot) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade_pulse))) - .floating(true) - .set(state.ids.m1_slot_act, ui); - } else { - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .top_left_with_margins_on( - state.ids.hotbar_align, - -40.0 * scale, - 0.0, - ) - .set(state.ids.m1_slot, ui); - } - }, - _ => { - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .top_left_with_margins_on(state.ids.hotbar_align, -40.0 * scale, 0.0) - .set(state.ids.m1_slot, ui); - }, - } - }, + // Skillbar + // Alignment and BG + Rectangle::fill_with([524.0, 80.0], color::TRANSPARENT) + .mid_bottom_with_margin_on(ui.window, 10.0) + .set(state.ids.alignment, ui); + Image::new(self.imgs.skillbar_bg) + .w_h(480.0, 80.0) + .color(Some(UI_MAIN)) + .middle_of(state.ids.alignment) + .set(state.ids.bg, ui); + // Level + let lvl_size = match self.stats.level.level() { + 11..=99 => 13, + 100..=999 => 10, + _ => 14, + }; + Text::new(&level) + .mid_top_with_margin_on(state.ids.bg, 3.0) + .font_size(self.fonts.cyri.scale(lvl_size)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.level, ui); + // Exp-Bar + Rectangle::fill_with([476.0, 8.0], color::TRANSPARENT) + .mid_bottom_with_margin_on(state.ids.bg, 4.0) + .set(state.ids.exp_alignment, ui); + Image::new(self.imgs.bar_content) + .w_h(476.0 * exp_percentage, 8.0) + .color(Some(XP_COLOR)) + .bottom_left_with_margins_on(state.ids.exp_alignment, 0.0, 0.0) + .set(state.ids.exp_filling, ui); + // Health and Stamina bar + // Alignment + Rectangle::fill_with([240.0, 17.0], color::TRANSPARENT) + .top_left_with_margins_on(state.ids.alignment, 0.0, 0.0) + .set(state.ids.hp_alignment, ui); + Rectangle::fill_with([240.0, 17.0], color::TRANSPARENT) + .top_right_with_margins_on(state.ids.alignment, 0.0, 0.0) + .set(state.ids.stamina_alignment, ui); + let health_col = match hp_percentage as u8 { + 0..=20 => crit_hp_color, + 21..=40 => LOW_HP_COLOR, + _ => HP_COLOR, + }; + // Content + Image::new(self.imgs.bar_content) + .w_h(216.0 * hp_percentage / 100.0, 14.0) + .color(Some(health_col)) + .top_right_with_margins_on(state.ids.hp_alignment, 4.0, 0.0) + .set(state.ids.hp_filling, ui); + Image::new(self.imgs.bar_content) + .w_h(216.0 * energy_percentage / 100.0, 14.0) + .color(Some(STAMINA_COLOR)) + .top_left_with_margins_on(state.ids.stamina_alignment, 4.0, 0.0) + .set(state.ids.stamina_filling, ui); + Rectangle::fill_with([219.0, 14.0], color::TRANSPARENT) + .top_left_with_margins_on(state.ids.hp_alignment, 4.0, 20.0) + .set(state.ids.hp_txt_alignment, ui); + Rectangle::fill_with([219.0, 14.0], color::TRANSPARENT) + .top_right_with_margins_on(state.ids.stamina_alignment, 4.0, 20.0) + .set(state.ids.stamina_txt_alignment, ui); + // Bar Text + // Values + if let BarNumbers::Values = bar_values { + let mut hp_txt = format!( + "{}/{}", + (self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for + * living players */ + (self.stats.health.maximum() / 10) as u32 + ); + let mut energy_txt = format!("{}", energy_percentage as u32); + if self.stats.is_dead { + hp_txt = self.localized_strings.get("hud.group.dead").to_string(); + energy_txt = self.localized_strings.get("hud.group.dead").to_string(); + }; + Text::new(&hp_txt) + .middle_of(state.ids.hp_txt_alignment) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.hp_txt_bg, ui); + Text::new(&hp_txt) + .bottom_left_with_margins_on(state.ids.hp_txt_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.hp_txt, ui); + Text::new(&energy_txt) + .middle_of(state.ids.stamina_txt_alignment) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.stamina_txt_bg, ui); + Text::new(&energy_txt) + .bottom_left_with_margins_on(state.ids.stamina_txt_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.stamina_txt, ui); } - // M1 Slot - Image::new(self.imgs.skillbar_slot_big_bg) - .w_h(38.0 * scale, 38.0 * scale) - .color( - match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(Tool { kind, .. })) => match kind { - ToolKind::Bow(_) => Some(BG_COLOR_2), - ToolKind::Staff(_) => Some(BG_COLOR_2), - _ => Some(BG_COLOR_2), - }, - _ => Some(BG_COLOR_2), - }, - ) - .middle_of(state.ids.m1_slot) - .set(state.ids.m1_slot_bg, ui); - Button::image( - match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(Tool { kind, .. })) => match kind { - ToolKind::Sword(_) => self.imgs.twohsword_m1, - ToolKind::Dagger(_) => self.imgs.onehdagger_m1, - ToolKind::Shield(_) => self.imgs.onehshield_m1, - ToolKind::Hammer(_) => self.imgs.twohhammer_m1, - ToolKind::Axe(_) => self.imgs.twohaxe_m1, - ToolKind::Bow(_) => self.imgs.bow_m1, - ToolKind::Sceptre(_) => self.imgs.heal_0, - ToolKind::Staff(_) => self.imgs.fireball, - ToolKind::Debug(kind) => match kind.as_ref() { - "Boost" => self.imgs.flyingrod_m1, - _ => self.imgs.nothing, - }, - _ => self.imgs.nothing, - }, - _ => self.imgs.nothing, - }, - ) // Insert Icon here - .w_h(32.0 * scale, 32.0 * scale) - .middle_of(state.ids.m1_slot_bg) - .set(state.ids.m1_content, ui); - // M2 Slot - match self.character_state { - CharacterState::BasicMelee { .. } => { - let fade_pulse = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.6; //Animation timer; - if self.controller.secondary.is_pressed() { - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .right_from(state.ids.m1_slot, 0.0) - .set(state.ids.m2_slot, ui); - Image::new(self.imgs.skillbar_slot_big_act) - .w_h(40.0 * scale, 40.0 * scale) - .middle_of(state.ids.m2_slot) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade_pulse))) - .floating(true) - .set(state.ids.m2_slot_act, ui); - } else { - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .right_from(state.ids.m1_slot, 0.0) - .set(state.ids.m2_slot, ui); - } - }, - _ => { - Image::new(self.imgs.skillbar_slot_big) - .w_h(40.0 * scale, 40.0 * scale) - .right_from(state.ids.m1_slot, 0.0) - .set(state.ids.m2_slot, ui); - }, + //Percentages + if let BarNumbers::Percent = bar_values { + let mut hp_txt = format!("{}%", hp_percentage as u32); + let mut energy_txt = format!("{}", energy_percentage as u32); + if self.stats.is_dead { + hp_txt = self.localized_strings.get("hud.group.dead").to_string(); + energy_txt = self.localized_strings.get("hud.group.dead").to_string(); + }; + Text::new(&hp_txt) + .middle_of(state.ids.hp_txt_alignment) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.hp_txt_bg, ui); + Text::new(&hp_txt) + .bottom_left_with_margins_on(state.ids.hp_txt_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.hp_txt, ui); + Text::new(&energy_txt) + .middle_of(state.ids.stamina_txt_alignment) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.stamina_txt_bg, ui); + Text::new(&energy_txt) + .bottom_left_with_margins_on(state.ids.stamina_txt_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.stamina_txt, ui); } - - let active_tool_kind = match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), - _ => None, - }; - - let second_tool_kind = match self.loadout.second_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), - _ => None, - }; - - let tool_kind = match ( - active_tool_kind.map(|tk| tk.hands()), - second_tool_kind.map(|tk| tk.hands()), - ) { - (Some(Hands::TwoHand), _) => active_tool_kind, - (_, Some(Hands::OneHand)) => second_tool_kind, - (_, _) => None, - }; - - Image::new(self.imgs.skillbar_slot_big_bg) - .w_h(38.0 * scale, 38.0 * scale) - .color(match tool_kind { - Some(ToolKind::Bow(_)) => Some(BG_COLOR_2), - Some(ToolKind::Staff(_)) => Some(BG_COLOR_2), - _ => Some(BG_COLOR_2), - }) - .middle_of(state.ids.m2_slot) - .set(state.ids.m2_slot_bg, ui); - Button::image(match tool_kind { - Some(ToolKind::Sword(_)) => self.imgs.twohsword_m2, - Some(ToolKind::Dagger(_)) => self.imgs.onehdagger_m2, - Some(ToolKind::Shield(_)) => self.imgs.onehshield_m2, - Some(ToolKind::Hammer(_)) => self.imgs.hammergolf, - Some(ToolKind::Axe(_)) => self.imgs.axespin, - Some(ToolKind::Bow(_)) => self.imgs.bow_m2, - Some(ToolKind::Sceptre(_)) => self.imgs.heal_bomb, - Some(ToolKind::Staff(_)) => self.imgs.flamethrower, - Some(ToolKind::Debug(kind)) => match kind.as_ref() { - "Boost" => self.imgs.flyingrod_m2, - _ => self.imgs.nothing, - }, - _ => self.imgs.nothing, - }) - .w_h(32.0 * scale, 32.0 * scale) - .middle_of(state.ids.m2_slot_bg) - .image_color(match tool_kind { - Some(ToolKind::Sword(_)) => { - if self.energy.current() as f64 >= 200.0 { - Color::Rgba(1.0, 1.0, 1.0, 1.0) - } else { - Color::Rgba(0.3, 0.3, 0.3, 0.8) - } - }, - Some(ToolKind::Sceptre(_)) => { - if self.energy.current() as f64 >= 400.0 { - Color::Rgba(1.0, 1.0, 1.0, 1.0) - } else { - Color::Rgba(0.3, 0.3, 0.3, 0.8) - } - }, - Some(ToolKind::Axe(_)) => { - if self.energy.current() as f64 >= 100.0 { - Color::Rgba(1.0, 1.0, 1.0, 1.0) - } else { - Color::Rgba(0.3, 0.3, 0.3, 0.8) - } - }, - _ => Color::Rgba(1.0, 1.0, 1.0, 1.0), - }) - .set(state.ids.m2_content, ui); // Slots let content_source = (self.hotbar, self.inventory, self.loadout, self.energy); // TODO: avoid this let image_source = (self.item_imgs, self.imgs); - let mut slot_maker = SlotMaker { // TODO: is a separate image needed for the frame? - empty_slot: self.imgs.skillbar_slot, - filled_slot: self.imgs.skillbar_slot, - selected_slot: self.imgs.skillbar_slot_act, + empty_slot: self.imgs.inv_slot, + filled_slot: self.imgs.inv_slot, + selected_slot: self.imgs.inv_slot_sel, background_color: None, content_size: ContentSize { width_height_ratio: 1.0, @@ -792,14 +524,6 @@ impl<'a> Widget for Skillbar<'a> { .map(|i| i.item.kind()) .and_then(|kind| match kind { ItemKind::Tool(Tool { kind, .. }) => match kind { - ToolKind::Hammer(_) => Some(( - "Smash of Doom", - "\nAn AOE attack with knockback. \nLeaps to position of \ - cursor.", - )), - ToolKind::Axe(_) => { - Some(("Spin Leap", "\nA slashing running spin leap.")) - }, ToolKind::Staff(_) => Some(( "Firebomb", "\nWhirls a big fireball into the air. \nExplodes the ground \ @@ -809,10 +533,6 @@ impl<'a> Widget for Skillbar<'a> { "Whirlwind", "\nMove forward while spinning with \n your sword.", )), - ToolKind::Bow(_) => Some(( - "Burst", - "\nLaunches a burst of arrows at the top \nof a running leap.", - )), ToolKind::Debug(kind) => match kind.as_ref() { "Boost" => Some(( "Possessing Arrow", @@ -827,71 +547,172 @@ impl<'a> Widget for Skillbar<'a> { }), }) }; - - //Slot 5 - let slot = slot_maker - .fabricate(hotbar::Slot::Five, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) - .bottom_left_with_margins_on(state.ids.m1_slot, 0.0, -20.0 * scale); - if let Some((title, desc)) = tooltip_text(hotbar::Slot::Five) { - slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) - .set(state.ids.slot5, ui); - } else { - slot.set(state.ids.slot5, ui); - } - // Slot 4 - let slot = slot_maker - .fabricate(hotbar::Slot::Four, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) - .left_from(state.ids.slot5, 0.0); - if let Some((title, desc)) = tooltip_text(hotbar::Slot::Four) { - slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) - .set(state.ids.slot4, ui); - } else { - slot.set(state.ids.slot4, ui); - } - // Slot 3 - let slot = slot_maker - .fabricate(hotbar::Slot::Three, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) - .left_from(state.ids.slot4, 0.0); - if let Some((title, desc)) = tooltip_text(hotbar::Slot::Three) { - slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) - .set(state.ids.slot3, ui); - } else { - slot.set(state.ids.slot3, ui); - } - // Slot 2 - let slot = slot_maker - .fabricate(hotbar::Slot::Two, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) - .left_from(state.ids.slot3, 0.0); - if let Some((title, desc)) = tooltip_text(hotbar::Slot::Two) { - slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) - .set(state.ids.slot2, ui); - } else { - slot.set(state.ids.slot2, ui); - } + // Slot 1-5 // Slot 1 - slot_maker.empty_slot = self.imgs.skillbar_slot_l; - slot_maker.selected_slot = self.imgs.skillbar_slot_l_act; + slot_maker.empty_slot = self.imgs.inv_slot; + slot_maker.selected_slot = self.imgs.inv_slot; let slot = slot_maker - .fabricate(hotbar::Slot::One, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot_l) - .left_from(state.ids.slot2, 0.0); + .fabricate(hotbar::Slot::One, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .bottom_left_with_margins_on(state.ids.frame, 15.0, 22.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::One) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) .set(state.ids.slot1, ui); } else { slot.set(state.ids.slot1, ui); } - // Slot 6 - slot_maker.empty_slot = self.imgs.skillbar_slot; - slot_maker.selected_slot = self.imgs.skillbar_slot_act; + // Slot 2 let slot = slot_maker - .fabricate(hotbar::Slot::Six, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) - .bottom_right_with_margins_on(state.ids.m2_slot, 0.0, -20.0 * scale); + .fabricate(hotbar::Slot::Two, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .right_from(state.ids.slot1, 0.0); + if let Some((title, desc)) = tooltip_text(hotbar::Slot::Two) { + slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) + .set(state.ids.slot2, ui); + } else { + slot.set(state.ids.slot2, ui); + } + // Slot 3 + let slot = slot_maker + .fabricate(hotbar::Slot::Three, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .right_from(state.ids.slot2, 0.0); + if let Some((title, desc)) = tooltip_text(hotbar::Slot::Three) { + slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) + .set(state.ids.slot3, ui); + } else { + slot.set(state.ids.slot3, ui); + } + // Slot 4 + let slot = slot_maker + .fabricate(hotbar::Slot::Four, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .right_from(state.ids.slot3, 0.0); + if let Some((title, desc)) = tooltip_text(hotbar::Slot::Three) { + slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) + .set(state.ids.slot4, ui); + } else { + slot.set(state.ids.slot4, ui); + } + // Slot 5 + let slot = slot_maker + .fabricate(hotbar::Slot::Five, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .right_from(state.ids.slot4, 0.0); + if let Some((title, desc)) = tooltip_text(hotbar::Slot::Three) { + slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) + .set(state.ids.slot5, ui); + } else { + slot.set(state.ids.slot5, ui); + } + // Slot M1 + Image::new(self.imgs.inv_slot) + .w_h(40.0, 40.0) + .right_from(state.ids.slot5, 0.0) + .set(state.ids.m1_slot_bg, ui); + Button::image( + match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { + Some(ItemKind::Tool(Tool { kind, .. })) => match kind { + ToolKind::Sword(_) => self.imgs.twohsword_m1, + ToolKind::Dagger(_) => self.imgs.onehdagger_m1, + ToolKind::Shield(_) => self.imgs.onehshield_m1, + ToolKind::Hammer(_) => self.imgs.twohhammer_m1, + ToolKind::Axe(_) => self.imgs.twohaxe_m1, + ToolKind::Bow(_) => self.imgs.bow_m1, + ToolKind::Sceptre(_) => self.imgs.heal_0, + ToolKind::Staff(_) => self.imgs.fireball, + ToolKind::Debug(kind) => match kind.as_ref() { + "Boost" => self.imgs.flyingrod_m1, + _ => self.imgs.nothing, + }, + _ => self.imgs.nothing, + }, + _ => self.imgs.nothing, + }, + ) // Insert Icon here + .w_h(36.0, 36.0) + .middle_of(state.ids.m1_slot_bg) + .set(state.ids.m1_content, ui); + // Slot M2 + Image::new(self.imgs.inv_slot) + .w_h(40.0, 40.0) + .right_from(state.ids.m1_slot_bg, 0.0) + .set(state.ids.m2_slot, ui); + + let active_tool_kind = match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { + Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), + _ => None, + }; + + let second_tool_kind = match self.loadout.second_item.as_ref().map(|i| i.item.kind()) { + Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), + _ => None, + }; + + let tool_kind = match ( + active_tool_kind.map(|tk| tk.hands()), + second_tool_kind.map(|tk| tk.hands()), + ) { + (Some(Hands::TwoHand), _) => active_tool_kind, + (_, Some(Hands::OneHand)) => second_tool_kind, + (_, _) => None, + }; + + Image::new(self.imgs.inv_slot) + .w_h(40.0, 40.0) + .middle_of(state.ids.m2_slot) + .set(state.ids.m2_slot_bg, ui); + Button::image(match tool_kind { + Some(ToolKind::Sword(_)) => self.imgs.twohsword_m2, + Some(ToolKind::Dagger(_)) => self.imgs.onehdagger_m2, + Some(ToolKind::Shield(_)) => self.imgs.onehshield_m2, + Some(ToolKind::Hammer(_)) => self.imgs.hammergolf, + Some(ToolKind::Axe(_)) => self.imgs.axespin, + Some(ToolKind::Bow(_)) => self.imgs.bow_m2, + Some(ToolKind::Sceptre(_)) => self.imgs.heal_bomb, + Some(ToolKind::Staff(_)) => self.imgs.flamethrower, + Some(ToolKind::Debug(kind)) => match kind.as_ref() { + "Boost" => self.imgs.flyingrod_m2, + _ => self.imgs.nothing, + }, + _ => self.imgs.nothing, + }) + .w_h(36.0, 36.0) + .middle_of(state.ids.m2_slot_bg) + .image_color(match tool_kind { + // TODO Automate this to grey out unavailable M2 skills + Some(ToolKind::Sword(_)) => { + if self.energy.current() as f64 >= 200.0 { + Color::Rgba(1.0, 1.0, 1.0, 1.0) + } else { + Color::Rgba(0.3, 0.3, 0.3, 0.8) + } + }, + Some(ToolKind::Sceptre(_)) => { + if self.energy.current() as f64 >= 400.0 { + Color::Rgba(1.0, 1.0, 1.0, 1.0) + } else { + Color::Rgba(0.3, 0.3, 0.3, 0.8) + } + }, + Some(ToolKind::Axe(_)) => { + if self.energy.current() as f64 >= 100.0 { + Color::Rgba(1.0, 1.0, 1.0, 1.0) + } else { + Color::Rgba(0.3, 0.3, 0.3, 0.8) + } + }, + _ => Color::Rgba(1.0, 1.0, 1.0, 1.0), + }) + .set(state.ids.m2_content, ui); + // Slot 6-10 + // Slot 6 + slot_maker.empty_slot = self.imgs.inv_slot; + slot_maker.selected_slot = self.imgs.inv_slot; + let slot = slot_maker + .fabricate(hotbar::Slot::Six, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) + .right_from(state.ids.m2_slot_bg, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::Six) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) .set(state.ids.slot6, ui); @@ -900,8 +721,8 @@ impl<'a> Widget for Skillbar<'a> { } // Slot 7 let slot = slot_maker - .fabricate(hotbar::Slot::Seven, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) + .fabricate(hotbar::Slot::Seven, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) .right_from(state.ids.slot6, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::Seven) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) @@ -911,8 +732,8 @@ impl<'a> Widget for Skillbar<'a> { } // Slot 8 let slot = slot_maker - .fabricate(hotbar::Slot::Eight, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) + .fabricate(hotbar::Slot::Eight, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) .right_from(state.ids.slot7, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::Eight) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) @@ -922,8 +743,8 @@ impl<'a> Widget for Skillbar<'a> { } // Slot 9 let slot = slot_maker - .fabricate(hotbar::Slot::Nine, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot) + .fabricate(hotbar::Slot::Nine, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) .right_from(state.ids.slot8, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::Nine) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) @@ -932,11 +753,11 @@ impl<'a> Widget for Skillbar<'a> { slot.set(state.ids.slot9, ui); } // Quickslot - slot_maker.empty_slot = self.imgs.skillbar_slot_r; - slot_maker.selected_slot = self.imgs.skillbar_slot_r_act; + slot_maker.empty_slot = self.imgs.inv_slot; + slot_maker.selected_slot = self.imgs.inv_slot; let slot = slot_maker - .fabricate(hotbar::Slot::Ten, [20.0 * scale as f32; 2]) - .filled_slot(self.imgs.skillbar_slot_r) + .fabricate(hotbar::Slot::Ten, [40.0; 2]) + .filled_slot(self.imgs.inv_slot) .right_from(state.ids.slot9, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::Ten) { slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) @@ -1042,7 +863,7 @@ impl<'a> Widget for Skillbar<'a> { .color(TEXT_COLOR) .set(state.ids.slot5_text, ui); } - if let Some(m1) = &self + /*if let Some(m1) = &self .global_state .settings .controls @@ -1079,7 +900,7 @@ impl<'a> Widget for Skillbar<'a> { .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.m2_text, ui); - } + }*/ if let Some(slot6) = &self .global_state .settings @@ -1176,114 +997,22 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.slot10_text, ui); } }; - - // Lifebar - Image::new(self.imgs.healthbar_bg) - .w_h(100.0 * scale, 20.0 * scale) - .top_left_with_margins_on(state.ids.m1_slot, 0.0, -100.0 * scale) - .set(state.ids.healthbar_bg, ui); - let health_col = match hp_percentage as u8 { - 0..=20 => crit_hp_color, - 21..=40 => LOW_HP_COLOR, - _ => HP_COLOR, - }; - Image::new(self.imgs.bar_content) - .w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale) - .color(Some(health_col)) - .top_right_with_margins_on(state.ids.healthbar_bg, 2.0 * scale, 1.0 * scale) - .set(state.ids.healthbar_filling, ui); - // Energybar - Image::new(self.imgs.energybar_bg) - .w_h(100.0 * scale, 20.0 * scale) - .top_right_with_margins_on(state.ids.m2_slot, 0.0, -100.0 * scale) - .set(state.ids.energybar_bg, ui); - Image::new(self.imgs.bar_content) - .w_h(97.0 * scale * energy_percentage / 100.0, 16.0 * scale) - .top_left_with_margins_on(state.ids.energybar_bg, 2.0 * scale, 1.0 * scale) - .color(Some(match self.current_resource { - ResourceType::Mana => MANA_COLOR, - /*ResourceType::Focus => FOCUS_COLOR, - *ResourceType::Rage => RAGE_COLOR, */ - })) - .set(state.ids.energybar_filling, ui); - // Bar Text - // Values - if let BarNumbers::Values = bar_values { - let mut hp_text = format!( - "{}/{}", - (self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for - * living players */ - (self.stats.health.maximum() / 10) as u32 - ); - let mut energy_text = format!( - "{}/{}", - self.energy.current() as u32 / 10, /* TODO Fix regeneration with smaller energy - * numbers instead of dividing by 10 here */ - self.energy.maximum() as u32 / 10 - ); - if self.stats.is_dead { - hp_text = self.localized_strings.get("hud.group.dead").to_string(); - energy_text = self.localized_strings.get("hud.group.dead").to_string(); - }; - Text::new(&hp_text) - .mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.health_text_bg, ui); - Text::new(&hp_text) - .bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.health_text, ui); - Text::new(&energy_text) - .mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.energy_text_bg, ui); - Text::new(&energy_text) - .bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.energy_text, ui); - } - //Percentages - if let BarNumbers::Percent = bar_values { - let hp_text = format!("{}%", hp_percentage as u32); - Text::new(&hp_text) - .mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.health_text_bg, ui); - Text::new(&hp_text) - .bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.health_text, ui); - let energy_text = format!("{}%", energy_percentage as u32); - Text::new(&energy_text) - .mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.energy_text_bg, ui); - Text::new(&energy_text) - .bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.energy_text, ui); - } + // Frame + Image::new(self.imgs.skillbar_frame) + .w_h(524.0, 80.0) + .color(Some(UI_HIGHLIGHT_0)) + .middle_of(state.ids.bg) + .floating(true) + .set(state.ids.frame, ui); + // M1 and M2 icons + // TODO Don't show this if key bindings are changed + Image::new(self.imgs.m1_ico) + .w_h(16.0, 18.0) + .mid_bottom_with_margin_on(state.ids.m1_content, -11.0) + .set(state.ids.m1_ico, ui); + Image::new(self.imgs.m2_ico) + .w_h(16.0, 18.0) + .mid_bottom_with_margin_on(state.ids.m2_content, -11.0) + .set(state.ids.m2_ico, ui); } - - // Buffs - // Add debuff slots above the health bar - // Add buff slots above the mana bar - - // Debuffs } diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index 81355d8996..8ce9fb4fcb 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -18,7 +18,6 @@ use conrod_core::{ widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, }; use image::DynamicImage; -//use inline_tweak::*; use rand::{seq::SliceRandom, thread_rng, Rng}; use std::time::Duration; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 9d752aeed3..d0b3d3a8c9 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -894,6 +894,10 @@ impl PlayState for SessionState { global_state.settings.gameplay.shortcut_numbers = shortcut_numbers; global_state.settings.save_to_file_warn(); }, + HudEvent::BuffPosition(buff_position) => { + global_state.settings.gameplay.buff_position = buff_position; + global_state.settings.save_to_file_warn(); + }, HudEvent::UiScale(scale_change) => { global_state.settings.gameplay.ui_scale = self.hud.scale_change(scale_change); @@ -921,6 +925,10 @@ impl PlayState for SessionState { global_state.settings.graphics.max_fps = fps; global_state.settings.save_to_file_warn(); }, + HudEvent::RemoveBuff(buff_id) => { + let mut client = self.client.borrow_mut(); + client.remove_buff(buff_id); + }, HudEvent::UseSlot(x) => self.client.borrow_mut().use_slot(x), HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b), HudEvent::DropSlot(x) => { diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index be4eed60db..1f65e800de 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -1,5 +1,5 @@ use crate::{ - hud::{BarNumbers, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar}, + hud::{BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar}, i18n, render::RenderMode, ui::ScaleMode, @@ -507,6 +507,7 @@ pub struct GameplaySettings { pub intro_show: Intro, pub xp_bar: XpBar, pub shortcut_numbers: ShortcutNumbers, + pub buff_position: BuffPosition, pub bar_numbers: BarNumbers, pub ui_scale: ScaleMode, pub free_look_behavior: PressBehavior, @@ -537,6 +538,7 @@ impl Default for GameplaySettings { intro_show: Intro::Show, xp_bar: XpBar::Always, shortcut_numbers: ShortcutNumbers::On, + buff_position: BuffPosition::Map, bar_numbers: BarNumbers::Values, ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()), free_look_behavior: PressBehavior::Toggle,