From ac17c57975a932b07aee1ba0425c94a3996a5cb2 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Fri, 16 Oct 2020 08:08:45 +0200 Subject: [PATCH] Initial implementation of buffs UI player buffs animation more testing debuffs sorting and display limit fix overhead buffs fix WIP buff removal function fmt Update buffs.rs Now with compiling: WIP group UI buffs positioning Update group.rs Update group.rs Small optimizations. Fixed positioning of buffs in group panel. Broke buff tooltips in group panel. buff frame visuals added setting for displaying buffs at minimap --- assets/common/items/food/apple_stick.ron | 2 +- .../element/animation/buff_frame/1.png | Bin 0 -> 2149 bytes .../element/animation/buff_frame/2.png | Bin 0 -> 2131 bytes .../element/animation/buff_frame/3.png | Bin 0 -> 2145 bytes .../element/animation/buff_frame/4.png | Bin 0 -> 2128 bytes .../element/animation/buff_frame/5.png | Bin 0 -> 2145 bytes .../element/animation/buff_frame/6.png | Bin 0 -> 2117 bytes .../element/animation/buff_frame/7.png | Bin 0 -> 2147 bytes .../element/animation/buff_frame/8.png | Bin 0 -> 2123 bytes .../{ => icons}/de_buffs/buff_plus_0.png | Bin .../element/icons/de_buffs/debuff_bleed_0.png | Bin 0 -> 228 bytes .../{ => icons}/de_buffs/debuff_skull_0.png | Bin assets/voxygen/i18n/en.ron | 18 +- client/src/lib.rs | 7 + common/src/comp/buff.rs | 14 +- common/src/comp/controller.rs | 7 +- common/src/sys/agent.rs | 2 +- common/src/sys/buff.rs | 2 +- common/src/sys/combat.rs | 21 +- common/src/sys/controller.rs | 10 +- server/src/events/entity_manipulation.rs | 23 +- voxygen/src/hud/buffs.rs | 298 +++++++++++++++--- voxygen/src/hud/group.rs | 244 +++++++++++--- voxygen/src/hud/img_ids.rs | 17 +- voxygen/src/hud/minimap.rs | 2 +- voxygen/src/hud/mod.rs | 109 +++++-- voxygen/src/hud/overhead.rs | 111 ++++++- voxygen/src/hud/settings_window.rs | 76 ++++- voxygen/src/session.rs | 8 + voxygen/src/settings.rs | 4 +- 30 files changed, 797 insertions(+), 178 deletions(-) create mode 100644 assets/voxygen/element/animation/buff_frame/1.png create mode 100644 assets/voxygen/element/animation/buff_frame/2.png create mode 100644 assets/voxygen/element/animation/buff_frame/3.png create mode 100644 assets/voxygen/element/animation/buff_frame/4.png create mode 100644 assets/voxygen/element/animation/buff_frame/5.png create mode 100644 assets/voxygen/element/animation/buff_frame/6.png create mode 100644 assets/voxygen/element/animation/buff_frame/7.png create mode 100644 assets/voxygen/element/animation/buff_frame/8.png rename assets/voxygen/element/{ => icons}/de_buffs/buff_plus_0.png (100%) create mode 100644 assets/voxygen/element/icons/de_buffs/debuff_bleed_0.png rename assets/voxygen/element/{ => icons}/de_buffs/debuff_skull_0.png (100%) 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/voxygen/element/animation/buff_frame/1.png b/assets/voxygen/element/animation/buff_frame/1.png new file mode 100644 index 0000000000000000000000000000000000000000..e05166cd464cb514d8e738eb98b153565f554ece GIT binary patch literal 2149 zcmcIm&2QX96gL6^Db1mP1VSJzS4e!#dOY^}OW9Vt-fgp}8=`cprUxX(Gvi$=yB=dZ z*-gZ$98eC369*6njy)kEai9W;T)0qiL;L~UD^3WGJnvW1l7y%dD}RpP{NC@q_j@zD zzrJ>L`P@tA6h&EXuQoT}{(N~p{UlsJ*?(mjZcFLvtz1!_d9FMkQ@;4}MMZh)*SNb` zY<8~sOb#oQ%TQFt!xXF)W##fXrEE(SYAAYfQq%tU{zpxXc}=@n?ch!thj&%XK8|j z>0_GeMg^B4%?&%9(@lrNg>_!EcEHPw@3FvncQ+OKhREf1#zbpJB*n$Kv$QV@nfK)h z+!mi7V943RVY;0SCh6f!7XvW_>>O;33BS}2GC>QOb)_843$;G?s1l<>)Yosu374b% zjgrx9f@so0)U>i{$bf@%3Gpr0H}PAT_!v)3JD>wj3;K|;)5X}g;IJMrmOPGj&tlr? z`0XSwXu?FhS<`@2C62kzLMm`I6o^JNM7C==h}xcwutz-0BA#uV?t~Z2Z%VdZW}=)w zl`WSHFc#VL9h58y+-akZhgTY+sr zP>56vP36pwWHzKAw0KB+LQj)kDSMy+;jYFx)OdH+NY}*&v!_9VJEv2p*oa(iS;z?;6GhAl zQD|ZhfopzZ>vT>TrB8dbzMp6{bHd zzC)GIJEaaiTuL*a)wDr-i|SrFzA;V04a`xH#{0Mb0qC)xLI3~& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..54c5183ec1047502eb0f01b8a3f245e3ec3711fd GIT binary patch literal 2131 zcmcIl&u`pB6gC2twvYpYLsg1oIYI)#tY^lK?Xj|n_E(dY*bt>FHQX4_jCZZs@a;97!{v@F#A(xXX9jhY`lOj4;Q#m5z+#Rk>p zrK($1)ewO%?Nb`MLFnL5Fby%DoAy8ll9&8BW517a=#mio&lqc&#HTM~+Utegv?zHh zRJUEXfK)9>Wau-dIP*A)h(}QryB^{g`$)vf5sU=P_cms{V1HYS<0=!?{<&Wj3BAx{SiPFUM|^&&%>VZapcCEGZv%d9BbK*0S_ot z8_oxZdB}^P9eEtPNI0&)fu)o#bFSXq>@<9e+s&548l46qL92mV9j_hG`^^UF;0rrv z4Cdk3*T+4xyfB)8^lS;{U3I$}wD7PR)hY$`ZWpL~A{hWLlwcB3CW7XLhby8Sx2 xb93vRZ+6!1ZDj5DcYpc)$&K&s2KUz9Fq(h=Nq;(f``c>lcRPFSpPOHP{V%)uk=OtL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b0a196aeac76d813c343c7d5fdad22599b9939b6 GIT binary patch literal 2145 zcmcIm&5Ptj6wi9lb!9|RK@=foFMd_0QmIZ-VS1PT+MPx-4zmqAf`XO$=nm7Jib-a= zXAXjkh%3n6MG#NxNzm&GdUuch1AFr#;=#L>{+ii!W?U9*I-N@8)$je@d%suJH@7yg zu3dQPf?*hIy^YSczCT}G=O5Rzwh`*Do5z6Tc<>opL97 zCuC||qg=(JHXUc$+A!8HPczDPMQO%jkfaUk&pSU`X2KiR^?C%OtSyGg#$hh59d7p7 z;VvWGx_rr8pN1M>EJ|ul$D_0erwwa{8|ra2ZCmEdq}**-&B~#<6K$Dol?&6WLBJqH zrcY{)hp`9WG7&`3hIQLP!0|$eL&rB4A4_}7xeT{ER~EkXQ^OjTWft1@WHPBu+?vV< zwnGT9A+iwy+5!{@X-TIbEiNt*I-+2Cl9h={%?gpmYOicqn&~`*akhd@i$$7r!t5!{ zY^MgRkd}r~bhhbuys|FJ?!NZ2;Cmu)(Lcz9y)6p0mow4b7ioEM=`0(nQWZmW3b)nk z`xtVza+vMqqgi@5v&Be^HFlwGof*F@Z0CZOD(@>bS{7<+>Cr?^4VkZ9Pg1TX#p@NL z#RAcxrD#}H)qtbFsE<&HgV2R&ZUK41=UkssG748)9=Uf)q{ugXNVelA}=7wi>BCQe0=(JwD%Sx3_X7laYQ=Y+r4ey>8oci01)R zb1ajyP^x@Pwa}6=9SA#12bJtK4IS=AQs^2VEE?&W_-HX2CFV?>A!SulG%QxBL2zp^ zot%*7hYM&9I~fWMdWK5oW`&aFgyy0-&?Wg#722nfFX;Z+)G2giz%d4XtYsn@jzR1~ zqUGy)gb~DbHI||OFVq)lWkZ?{gx+Co>(PUGIJuu#hkrNVqtxN#VC-sRoh!_KP<%%! zUv^4U_i!c6Vo}o;9jvN*>EyzWoior4Mnp*J<{HU;nuK z>N&%F#`xjyTW^1I`>`8^EeGdx TUw57APe8A`+4;Wp;f;R)lA)Wl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..beceaf397b15dcbb314adb9cb14199a28acdaff2 GIT binary patch literal 2128 zcmcIl&2HpG5H_r|EQ@kj;IKj6i>dfeUa@t-n@=5Ldcm=LlfZMXpK?)Hq88Mm>W z%p^x74twAQfER!RFTerbfXhm}0avcvmh${1yCfk3F`5~-+tpuvRrQtY$^PEX#@)B> z8iuja+wB~{_08&c=QjL*{p6huxV)O}eo`35Yj0G)myB<|yKfk;JWU3N z?JaY276QOTmE4?7#%U4G8rB>)gmtxTTjt!PJZf0Y%AtAK-#6PjSEgITNMKA&FQ^fh zF&BSeQcSUp>o%c?xFKdC@yzAN0&lsD!-LMlr7yT^SfjGcLff8Br?sh5)A`UQK@ixO z+LR)&K*ecV@)=5t2P=e*Dny=SWujBFLgbM?E*lmwU4$^n*05=@OcNx`p7G2kHC%OIer$-7~jnxFO3$P_~ep%Sg6)$x@(0aTo)c(q&NlA^G?>!fiE3U8Ztk0@cWpC7%xowpaaQE{(`YLz&KO3FOSROAFJw2$%xI){%hqE@UVAOO$BTpeOU>NxU z4G{NmOoYQZsaT!+ZR@el`&vSFos0Xuc6&e9aWaMn#lcR?>~-5N30xP^8nH|%!dT}M z4nj*Ne5mX!9agdj8W8SoQb3JQmyPsDeX(4P6LYT4kc+A*8kVTkprp0jPA*CF%>r7$ zPDctruTjautk7{e<+*AOp(OvPLi;N61wXl-I>#jTC1c2oJO{;sMJRG`0P=O*Km=4m zHCCbjFVvT56(gPw6}(|>>pui@Be`E%hrc)Azp2CJ!PwP{b)hi(Me&`heAOvU=;2D5 z#j>U?K3rFK>+;5Qs3gpX_j`+Z_ZhalXI+9JNn$GP;Cl@ z4_xF%(2hKg84?cj>R3w20_W<}t!~q!xYKGoL9^Q=#BVooyX$s*`mog`U3}r>oWUX- z`}(-&mKR17ke+YBysMV$K?^tQQLR!?PY#3lvrl(^gCTD9I?aK9PyG4p+4e1R|IXIE puXZ-xu4hkw`S=zrzQ6sl@$2K)e@E^Q-&Qlf*WK&<*!uj-zW^Duk`Vv^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4cb8dc04c1fc75e14513180a84ad33102b03bb97 GIT binary patch literal 2145 zcmcIm&2QX96kntYXdyxZ4yY26{VR z4oKtzJ#gfLKtkfe33^0A2oBufz=1pb0|>!^D?IO4(vpOz607xkJbv?gzxUqn&FtOo z=8ct0moHT+m6i5Ja|`azme=Et!tV!npI?F7#dPCNUa36sOnE(0`RL>4DwW56j(gk1 zcIUN_sgXgsii9y4rC?pDtY4d?lmV#7^xi5IC>Vu+4L(?3O$Hv$; zRMt1GAP7uMOhOP?pnN|m=maJC>H?uDa+bwu5vxQi5ox4$i>eMxXCaKzC2W$<(*y}K zCp0xJ1D7E!3_G2(O-G}pbzZdgz{{NPiNJYpKNaSd$klGfL~BnZ#p=RYI#7km2kI1V z%h&fYiokNGmQA<`F5Ccg(BAJJ1 zj4n}@J0z^G5EoM_{Nup~qANYQ-$Vf2s*07kM_4%!yEB z;{fDqy8#ObhiWWB|6i!j)5-=k=?mClP5t46IiK85ti!(>@L}q3axiAOvCb4`J}AB; zl`lG_20dI#GoRPAPW#L1UOl-nZNe?bhy7h*#*yPV7Bp03BNn+XcA1AGN3Ks<4C5oL zMLYV`Cs1tyg%4chM$nE_N(V99@zyZs+zIT{SJzuLkKks#VF$HV&9eMP4L4eD(t(-ELg=3x__tf&((FCNYQ*hpu!{tE>56h!kB%q!Q4&v@N@4pI%xZQ5ndj1c; z{b;RJ@RQ%&`}4h>3!nY8`pe}jU-tjH^UkmD{(X3}d-0{(3!h*3@YAPzhd1wizy8Tv We?0YuVSZKq323)An_tx5Jp2bGhMsHy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4bc7af2a3634879d63b1994a62bf93076a8bb3f2 GIT binary patch literal 2117 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 literal 0 HcmV?d00001 diff --git a/assets/voxygen/element/de_buffs/buff_plus_0.png b/assets/voxygen/element/icons/de_buffs/buff_plus_0.png similarity index 100% rename from assets/voxygen/element/de_buffs/buff_plus_0.png rename to assets/voxygen/element/icons/de_buffs/buff_plus_0.png 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 0000000000000000000000000000000000000000..4638eeb618e80a1d374b6721ade2c2aae045da8b GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE&r>HJ%+$Qd*2L2+ z*>c9syfT63$TOvwbtkb%Zi3^?&s=Qcw2UgL}xavouF{aA?TDb53}dR9&gJ a85ll?e%<@(hn5=9*$kepelF{r5}E);U{dw~ literal 0 HcmV?d00001 diff --git a/assets/voxygen/element/de_buffs/debuff_skull_0.png b/assets/voxygen/element/icons/de_buffs/debuff_skull_0.png similarity index 100% rename from assets/voxygen/element/de_buffs/debuff_skull_0.png rename to assets/voxygen/element/icons/de_buffs/debuff_skull_0.png diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 55355503b8..c85ffe8d27 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -291,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", @@ -343,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", @@ -509,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/client/src/lib.rs b/client/src/lib.rs index ad47263340..9342123523 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::BuffId; use futures_executor::block_on; use futures_timer::Delay; use futures_util::{select, FutureExt}; @@ -631,6 +632,12 @@ impl Client { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern)); } + pub fn remove_buff(&mut self, buff_id: BuffId) { + 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)> { diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index ca1d690002..828cf39d96 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -151,16 +151,10 @@ impl Buff { pub fn new(id: BuffId, cat_ids: Vec, source: BuffSource) -> Self { let (effects, time) = match id { BuffId::Bleeding { strength, duration } => ( - vec![ - BuffEffect::HealthChangeOverTime { - rate: -strength, - accumulated: 0.0, - }, - // This effect is for testing purposes - BuffEffect::NameChange { - prefix: String::from("Injured "), - }, - ], + vec![BuffEffect::HealthChangeOverTime { + rate: -strength, + accumulated: 0.0, + }], duration, ), BuffId::Regeneration { strength, duration } => ( diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index b9c7e1cb4c..64cdf93505 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, BuffId}, + 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(BuffId), Respawn, } diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 06d39cb1c8..63f263a765 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -684,7 +684,7 @@ impl<'a> System<'a> for Sys { for (_invite, /*alignment,*/ agent, controller) in (&invites, /*&alignments,*/ &mut agents, &mut controllers).join() { - let accept = false; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics + let accept = true; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics if accept { // Clear agent comp *agent = Agent::default(); diff --git a/common/src/sys/buff.rs b/common/src/sys/buff.rs index e509d5114b..a959ef3729 100644 --- a/common/src/sys/buff.rs +++ b/common/src/sys/buff.rs @@ -62,7 +62,7 @@ impl<'a> System<'a> for Sys { BuffEffect::HealthChangeOverTime { rate, accumulated } => { *accumulated += *rate * buff_delta; // Apply only 0.5 or higher damage - if accumulated.abs() > 5.0 { + if accumulated.abs() > 50.0 { let cause = if *accumulated > 0.0 { HealthSource::Healing { by: buff_owner } } else { diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index 6e46d94d6d..7eb6cd53a7 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -157,12 +157,31 @@ impl<'a> System<'a> for Sys { buff_change: buff::BuffChange::Add(buff::Buff::new( buff::BuffId::Bleeding { strength: attack.base_damage as f32, - duration: Some(Duration::from_secs(10)), + duration: Some(Duration::from_secs(30)), }, vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Debuff], buff::BuffSource::Character { by: *uid }, )), }); + server_emitter.emit(ServerEvent::Buff { + uid: *uid_b, + buff_change: buff::BuffChange::Add(buff::Buff::new( + buff::BuffId::Regeneration { + strength: 100.0, + duration: Some(Duration::from_secs(60)), + }, + vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Buff], + buff::BuffSource::Character { by: *uid }, + )), + }); + server_emitter.emit(ServerEvent::Buff { + uid: *uid_b, + buff_change: buff::BuffChange::Add(buff::Buff::new( + buff::BuffId::Cursed { duration: None }, + vec![buff::BuffCategoryId::Physical, buff::BuffCategoryId::Debuff], + buff::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..03fbf46c6e 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, @@ -51,7 +51,7 @@ impl<'a> System<'a> for Sys { span!(_guard, "run", "controller::Sys::run"); let mut server_emitter = server_bus.emitter(); - for (entity, _uid, controller, character_state) in + for (entity, uid, controller, character_state) in (&entities, &uids, &mut controllers, &mut character_states).join() { let mut inputs = &mut controller.inputs; @@ -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 { + uid: *uid, + buff_change: BuffChange::RemoveById(buff_id), + }); + }, ControlEvent::Unmount => server_emitter.emit(ServerEvent::Unmount(entity)), ControlEvent::EnableLantern => { server_emitter.emit(ServerEvent::EnableLantern(entity)) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d0ba6ee4d6..8d7e1b106e 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -715,6 +715,7 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange) add_buff_effects(new_buff.clone(), stats.get_mut(entity)); buffs.active_buffs.push(new_buff); } else { + let mut duplicate_existed = false; for i in 0..buffs.active_buffs.len() { let active_buff = &buffs.active_buffs[i]; // Checks if new buff has the same id as an already active buff. If it @@ -724,6 +725,7 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange) // inactive buffs and add new buff to active // buffs. if discriminant(&active_buff.id) == discriminant(&new_buff.id) { + duplicate_existed = true; if determine_replace_active_buff( active_buff.clone(), new_buff.clone(), @@ -731,14 +733,21 @@ pub fn handle_buff(server: &mut Server, uid: Uid, buff_change: buff::BuffChange) active_buff_indices_for_removal.push(i); add_buff_effects(new_buff.clone(), stats.get_mut(entity)); buffs.active_buffs.push(new_buff.clone()); - } else { - buffs.inactive_buffs.push(new_buff.clone()); + } else if let Some(active_dur) = active_buff.time { + if let Some(new_dur) = new_buff.time { + if new_dur > active_dur { + buffs.inactive_buffs.push(new_buff.clone()); + } + } else { + buffs.inactive_buffs.push(new_buff.clone()); + } } - } else { - add_buff_effects(new_buff.clone(), stats.get_mut(entity)); - buffs.active_buffs.push(new_buff.clone()); } } + if !duplicate_existed { + add_buff_effects(new_buff.clone(), stats.get_mut(entity)); + buffs.active_buffs.push(new_buff.clone()); + } } }, BuffChange::RemoveByIndex(active_indices, inactive_indices) => { @@ -871,7 +880,7 @@ fn determine_replace_active_buff(active_buff: buff::Buff, new_buff: buff::Buff) duration: _, } = active_buff.id { - new_strength > active_strength + new_strength >= active_strength } else { false } @@ -885,7 +894,7 @@ fn determine_replace_active_buff(active_buff: buff::Buff, new_buff: buff::Buff) duration: _, } = active_buff.id { - new_strength > active_strength + new_strength >= active_strength } else { false } diff --git a/voxygen/src/hud/buffs.rs b/voxygen/src/hud/buffs.rs index ad32915443..15a6552399 100644 --- a/voxygen/src/hud/buffs.rs +++ b/voxygen/src/hud/buffs.rs @@ -1,20 +1,23 @@ use super::{ img_ids::{Imgs, ImgsRot}, - TEXT_COLOR, + BUFF_COLOR, DEBUFF_COLOR, TEXT_COLOR, }; use crate::{ + hud::{get_buff_info, BuffPosition}, i18n::VoxygenLocalization, ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, GlobalState, }; -use client::Client; -use common::comp::{self, Buffs}; + +use crate::hud::BuffInfo; +use common::comp::{BuffId, Buffs}; use conrod_core::{ color, - widget::{self, Button, Image, Rectangle, Text}, - widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, + widget::{self, Button, Image, Rectangle}, + widget_ids, Color, Positionable, Sizeable, Widget, WidgetCommon, }; use inline_tweak::*; +use std::time::Duration; widget_ids! { struct Ids { align, @@ -22,51 +25,49 @@ widget_ids! { debuffs_align, buff_test, debuff_test, + buffs[], + buff_timers[], + debuffs[], + debuff_timers[], } } -pub struct BuffInfo { - id: comp::BuffId, - is_buff: bool, - dur: f32, -} - #[derive(WidgetCommon)] pub struct BuffsBar<'a> { - client: &'a Client, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, #[conrod(common_builder)] common: widget::CommonBuilder, - global_state: &'a GlobalState, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, localized_strings: &'a std::sync::Arc, 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( - client: &'a Client, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, - global_state: &'a GlobalState, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, localized_strings: &'a std::sync::Arc, buffs: &'a Buffs, + pulse: f32, + global_state: &'a GlobalState, ) -> Self { Self { - client, imgs, fonts, common: widget::CommonBuilder::default(), - global_state, rot_imgs, tooltip_manager, localized_strings, buffs, + pulse, + global_state, } } } @@ -75,8 +76,12 @@ pub struct State { ids: Ids, } +pub enum Event { + RemoveBuff(BuffId), +} + impl<'a> Widget for BuffsBar<'a> { - type Event = (); + type Event = Vec; type State = State; type Style = (); @@ -91,7 +96,11 @@ impl<'a> Widget for BuffsBar<'a> { fn update(self, args: widget::UpdateArgs) -> 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 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] @@ -109,35 +118,230 @@ impl<'a> Widget for BuffsBar<'a> { .desc_font_size(self.fonts.cyri.scale(12)) .font_id(self.fonts.cyri.conrod_id) .desc_text_color(TEXT_COLOR); - // Alignment - Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT) - .mid_bottom_with_margin_on(ui.window, tweak!(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); - // Test Widgets - Image::new(self.imgs.debuff_skull_0) - .w_h(20.0, 20.0) - .bottom_right_with_margins_on(state.ids.debuffs_align, 0.0, 1.0) - .set(state.ids.debuff_test, ui); - Image::new(self.imgs.buff_plus_0) - .w_h(20.0, 20.0) - .bottom_left_with_margins_on(state.ids.buffs_align, 0.0, 1.0) - .set(state.ids.buff_test, ui); - } -} + if let BuffPosition::Bar = buff_position { + // Alignment + Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT) + .mid_bottom_with_margin_on(ui.window, tweak!(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); -fn get_buff_info(buff: comp::Buff) -> BuffInfo { - BuffInfo { - id: buff.id, - is_buff: buff - .cat_ids - .iter() - .any(|cat| *cat == comp::BuffCategoryId::Buff), - dur: buff.time.map(|dur| dur.as_secs_f32()).unwrap_or(100.0), + // Buffs and Debuffs + // Create two vecs to display buffs and debuffs separately + let mut buffs_vec = Vec::::new(); + let mut debuffs_vec = Vec::::new(); + for buff in buffs.active_buffs.clone() { + let info = get_buff_info(buff); + if info.is_buff { + buffs_vec.push(info); + } else { + debuffs_vec.push(info); + } + } + if state.ids.buffs.len() < buffs_vec.len() { + state.update(|state| { + state + .ids + .buffs + .resize(buffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + if state.ids.debuffs.len() < debuffs_vec.len() { + state.update(|state| { + state + .ids + .debuffs + .resize(debuffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + if state.ids.buff_timers.len() < buffs_vec.len() { + state.update(|state| { + state + .ids + .buff_timers + .resize(buffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + if state.ids.debuff_timers.len() < debuffs_vec.len() { + state.update(|state| { + state + .ids + .debuff_timers + .resize(debuffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + 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 + for (i, buff) in buffs_vec.iter().enumerate() { + if i < 22 { + // Limit displayed buffs + let max_duration = match buff.id { + BuffId::Regeneration { duration, .. } => duration.unwrap().as_secs_f32(), + _ => 10.0, + }; + let current_duration = buff.dur; + let duration_percentage = (current_duration / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed + let buff_img = match buff.id { + BuffId::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 < 10.0 { + Some(pulsating_col) + } else { + Some(norm_col) + }) + .set(state.ids.buffs[i], ui); + // Create Buff tooltip + let title = match buff.id { + BuffId::Regeneration { .. } => { + *&localized_strings.get("buff.title.heal_test") + }, + _ => *&localized_strings.get("buff.title.missing"), + }; + let remaining_time = if current_duration == 10e6 as f32 { + "Permanent".to_string() + } else { + format!("Remaining: {:.0}s", current_duration) + }; + let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove")); + let desc_txt = match buff.id { + BuffId::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(state.ids.buffs[i]) + .with_tooltip( + self.tooltip_manager, + title, + &desc, + &buffs_tooltip, + BUFF_COLOR, + ) + .set(state.ids.buff_timers[i], ui) + .was_clicked() + { + event.push(Event::RemoveBuff(buff.id)); + }; + }; + } + // Create Debuff Widgets + for (i, debuff) in debuffs_vec.iter().enumerate() { + if i < 22 { + // Limit displayed buffs + + let max_duration = match debuff.id { + BuffId::Bleeding { duration, .. } => { + duration.unwrap_or(Duration::from_secs(60)).as_secs_f32() + }, + BuffId::Cursed { duration, .. } => { + duration.unwrap_or(Duration::from_secs(60)).as_secs_f32() + }, + + _ => 10.0, + }; + let current_duration = debuff.dur; + let duration_percentage = current_duration / max_duration * 1000.0; // Percentage to determine which frame of the timer overlay is displayed + let debuff_img = match debuff.id { + BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0, + BuffId::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 < 10.0 { + Some(pulsating_col) + } else { + Some(norm_col) + }) + .set(state.ids.debuffs[i], ui); + // Create Debuff tooltip + let title = match debuff.id { + BuffId::Bleeding { .. } => { + *&localized_strings.get("debuff.title.bleed_test") + }, + _ => *&localized_strings.get("buff.title.missing"), + }; + let remaining_time = if current_duration == 10e6 as f32 { + "Permanent".to_string() + } else { + format!("Remaining: {:.0}s", current_duration) + }; + let desc_txt = match debuff.id { + BuffId::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(state.ids.debuffs[i]) + .with_tooltip( + self.tooltip_manager, + title, + &desc, + &buffs_tooltip, + DEBUFF_COLOR, + ) + .set(state.ids.debuff_timers[i], ui); + }; + } + } + if let BuffPosition::Map = buff_position { + // Alignment + Rectangle::fill_with([tweak!(300.0), tweak!(280.0)], color::RED) + .top_right_with_margins_on(ui.window, tweak!(5.0), tweak!(270.0)) + .set(state.ids.align, ui); + } + event } } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 26b853cd08..06503bc9d8 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, - STAMINA_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, BuffInfo}, + 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, BuffId, Buffs, Stats}, sync::{Uid, WorldSyncExt}, }; use conrod_core::{ @@ -18,8 +23,8 @@ use conrod_core::{ widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; +use inline_tweak::*; use specs::{saveload::MarkerAllocator, WorldExt}; - widget_ids! { pub struct Ids { group_button, @@ -44,6 +49,8 @@ widget_ids! { member_panels_txt[], member_health[], member_stam[], + buffs[], + buff_timers[], dead_txt[], health_txt[], timeout_bg, @@ -63,10 +70,13 @@ 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, pulse: f32, global_state: &'a GlobalState, + buffs: &'a Buffs, + tooltip_manager: &'a mut TooltipManager, #[conrod(common_builder)] common: widget::CommonBuilder, @@ -79,20 +89,26 @@ 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, pulse: f32, global_state: &'a GlobalState, + buffs: &'a Buffs, + tooltip_manager: &'a mut TooltipManager, ) -> Self { Self { show, client, settings, imgs, + rot_imgs, fonts, localized_strings, pulse, global_state, + buffs, + tooltip_manager, common: widget::CommonBuilder::default(), } } @@ -127,8 +143,27 @@ impl<'a> Widget for Group<'a> { #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; - let mut events = 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 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,6 +328,7 @@ impl<'a> Widget for Group<'a> { let client_state = self.client.state(); let stats = client_state.ecs().read_storage::(); let energy = client_state.ecs().read_storage::(); + let buffs = client_state.ecs().read_storage::(); let uid_allocator = client_state .ecs() .read_resource::(); @@ -302,6 +338,8 @@ impl<'a> Widget for Group<'a> { 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; @@ -317,7 +355,7 @@ impl<'a> Widget for Group<'a> { .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,19 +424,19 @@ 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 @@ -408,44 +446,146 @@ impl<'a> Widget for Group<'a> { .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 { + let mut buffs_vec = Vec::::new(); + for buff in buffs.active_buffs.clone() { + let info = get_buff_info(buff); + buffs_vec.push(info); + } + state.update(|state| { + state.ids.buffs.resize( + state.ids.buffs.len() + buffs_vec.len(), + &mut ui.widget_id_generator(), + ) + }); + state.update(|state| { + state.ids.buff_timers.resize( + state.ids.buff_timers.len() + buffs_vec.len(), + &mut ui.widget_id_generator(), + ) + }); + // Create Buff Widgets + for (x, buff) in buffs_vec.iter().enumerate() { + if x < 11 { + // Limit displayed buffs + let max_duration = match buff.id { + BuffId::Regeneration { duration, .. } => { + duration.unwrap().as_secs_f32() + }, + _ => 10.0, + }; + 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 / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed + let buff_img = match buff.id { + BuffId::Regeneration { .. } => self.imgs.buff_plus_0, + BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0, + BuffId::Cursed { .. } => self.imgs.debuff_skull_0, + }; + let buff_widget = Image::new(buff_img).w_h(20.0, 20.0); + let buff_widget = if x == 0 { + buff_widget.bottom_left_with_margins_on( + state.ids.member_panels_frame[i], + -21.0, + 1.0, + ) + } else { + buff_widget.right_from(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x - 1/*x - 1*/], 1.0) + }; + buff_widget + .color(if current_duration < 10.0 { + Some(pulsating_col) + } else { + Some(norm_col) + }) + .set(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x/*x*/], ui); + // Create Buff tooltip + let title = match buff.id { + BuffId::Regeneration { .. } => { + *&localized_strings.get("buff.title.heal_test") + }, + BuffId::Bleeding { .. } => { + *&localized_strings.get("debuff.title.bleed_test") + }, + _ => *&localized_strings.get("buff.title.missing"), + }; + let remaining_time = if current_duration == 10e6 as f32 { + "Permanent".to_string() + } else { + format!("Remaining: {:.0}s", current_duration) + }; + let desc_txt = match buff.id { + BuffId::Regeneration { .. } => { + *&localized_strings.get("buff.desc.heal_test") + }, + BuffId::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(20.0, 20.0) + .middle_of(state.ids.buffs[state.ids.buffs.len() - buffs_vec.len() + x/*x*/]) + .with_tooltip( + self.tooltip_manager, + title, + &desc, + &buffs_tooltip, + if buff.is_buff {BUFF_COLOR} else {DEBUFF_COLOR}, + ) + .set(state.ids.buff_timers[state.ids.buffs.len() - buffs_vec.len() + x/*x*/], 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 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); + } } } diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 3aced27a57..fbb48033c0 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -272,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", @@ -350,10 +351,22 @@ image_ids! { chat_world: "voxygen.element.icons.chat.world", // Buffs - buff_plus_0: "voxygen.element.de_buffs.buff_plus_0", + buff_plus_0: "voxygen.element.icons.de_buffs.buff_plus_0", // Debuffs - debuff_skull_0: "voxygen.element.de_buffs.debuff_skull_0", + 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", 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::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 bd3d0575c4..1576fb3ff1 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -60,7 +60,10 @@ use client::Client; use common::{ assets::Asset, comp, - comp::item::{ItemDesc, Quality}, + comp::{ + item::{ItemDesc, Quality}, + BuffId, + }, span, sync::Uid, terrain::TerrainChunk, @@ -97,6 +100,8 @@ 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 @@ -267,6 +272,13 @@ widget_ids! { } } +#[derive(Clone, Copy)] +pub struct BuffInfo { + id: comp::BuffId, + is_buff: bool, + dur: f32, +} + pub struct DebugInfo { pub tps: f64, pub frame_time: Duration, @@ -318,6 +330,7 @@ pub enum Event { ChatTransp(f32), ChatCharName(bool), CrosshairType(CrosshairType), + BuffPosition(BuffPosition), ToggleXpBar(XpBar), Intro(Intro), ToggleBarNumbers(BarNumbers), @@ -351,6 +364,7 @@ pub enum Event { KickMember(common::sync::Uid), LeaveGroup, AssignLeader(common::sync::Uid), + RemoveBuff(BuffId), } // TODO: Are these the possible layouts we want? @@ -391,6 +405,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, @@ -725,6 +746,7 @@ impl Hud { let ecs = client.state().ecs(); let pos = ecs.read_storage::(); let stats = ecs.read_storage::(); + let buffs = ecs.read_storage::(); let energy = ecs.read_storage::(); let hp_floater_lists = ecs.read_storage::(); let uids = ecs.read_storage::(); @@ -1123,11 +1145,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, buffs, height_offset, hpfl, in_group) in ( &entities, &pos, interpolated.maybe(), &stats, + &buffs, energy.maybe(), scales.maybe(), &bodies, @@ -1141,7 +1164,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); @@ -1171,6 +1194,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) { @@ -1185,6 +1209,7 @@ impl Hud { info, bubble, stats, + buffs, body.height() * scale.map_or(1.0, |s| s.0) + 0.5, hpfl, in_group, @@ -1760,22 +1785,48 @@ impl Hud { // Buffs and Debuffs if let Some(player_buffs) = buffs.get(client.entity()) { - match BuffsBar::new( - client, + for event in BuffsBar::new( &self.imgs, &self.fonts, - global_state, &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 + let buffs = buffs.get(client.entity()).unwrap(); + 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, + &buffs, + 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, @@ -1850,8 +1901,8 @@ impl Hud { Some(stats), Some(loadout), Some(energy), - Some(character_state), - Some(controller), + Some(_character_state), + Some(_controller), Some(inventory), ) = ( stats.get(entity), @@ -2018,6 +2069,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)); }, @@ -2142,27 +2196,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 { @@ -2694,3 +2727,17 @@ pub fn get_quality_col(item: &I) -> Color { Quality::Debug => QUALITY_DEBUG, } } +// Get info about applied buffs +fn get_buff_info(buff: comp::Buff) -> BuffInfo { + BuffInfo { + id: buff.id, + is_buff: buff + .cat_ids + .iter() + .any(|cat| *cat == comp::BuffCategoryId::Buff), + dur: buff + .time + .map(|dur| dur.as_secs_f32()) + .unwrap_or(10e6 as f32), + } +} diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index e24e4eb853..8ee435ea1f 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -3,16 +3,19 @@ use super::{ REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR, }; use crate::{ + hud::{get_buff_info, BuffInfo}, i18n::VoxygenLocalization, settings::GameplaySettings, ui::{fonts::ConrodVoxygenFonts, Ingameable}, }; -use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats}; +use common::comp::{BuffId, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats}; use conrod_core::{ + color, position::Align, widget::{self, Image, Rectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; +use inline_tweak::*; const MAX_BUBBLE_WIDTH: f64 = 250.0; widget_ids! { @@ -44,13 +47,24 @@ widget_ids! { health_txt, mana_bar, health_bar_fg, + + // Buffs + buffs_align, + buffs[], + buff_timers[], } } +/*pub struct BuffInfo { + id: comp::BuffId, + dur: f32, +}*/ + #[derive(Clone, Copy)] pub struct Info<'a> { pub name: &'a str, pub stats: &'a Stats, + pub buffs: &'a Buffs, pub energy: Option<&'a Energy>, } @@ -119,17 +133,21 @@ 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 // 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 + + info.buffs.active_buffs.len().min(10) * 2 + + 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 +173,7 @@ impl<'a> Widget for Overhead<'a> { if let Some(Info { name, stats, + buffs, energy, }) = self.info { @@ -172,6 +191,11 @@ impl<'a> Widget for Overhead<'a> { } else { MANA_BAR_Y + 32.0 }; + let mut buffs_vec = Vec::::new(); + for buff in buffs.active_buffs.clone() { + let info = get_buff_info(buff); + buffs_vec.push(info); + } let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 }; // Show K for numbers above 10^3 and truncate them // Show M for numbers above 10^6 and truncate them @@ -185,6 +209,79 @@ 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 + Rectangle::fill_with([tweak!(168.0), tweak!(100.0)], color::TRANSPARENT) + .x_y(-1.0, name_y + tweak!(60.0)) + .parent(id) + .set(state.ids.buffs_align, ui); + if state.ids.buffs.len() < buffs_vec.len() { + state.update(|state| { + state + .ids + .buffs + .resize(buffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + if state.ids.buff_timers.len() < buffs_vec.len() { + state.update(|state| { + state + .ids + .buff_timers + .resize(buffs_vec.len(), &mut ui.widget_id_generator()) + }); + }; + 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 + for (i, buff) in buffs_vec.iter().enumerate() { + if i < 11 && self.bubble.is_none() { + // Limit displayed buffs + let max_duration = match buff.id { + BuffId::Regeneration { duration, .. } => duration.unwrap().as_secs_f32(), + _ => 10.0, + }; + let current_duration = buff.dur; + let duration_percentage = (current_duration / max_duration * 1000.0) as u32; // Percentage to determine which frame of the timer overlay is displayed + let buff_img = match buff.id { + BuffId::Regeneration { .. } => self.imgs.buff_plus_0, + BuffId::Bleeding { .. } => self.imgs.debuff_bleed_0, + BuffId::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 < 10.0 { + Some(pulsating_col) + } else { + Some(norm_col) + }) + .set(state.ids.buffs[i], 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(state.ids.buffs[i]) + .set(state.ids.buff_timers[i], ui); + }; + } // Name Text::new(name) .font_id(self.fonts.cyri.conrod_id) diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 9bf18b9473..b2975c7d52 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -4,6 +4,7 @@ use super::{ 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}, @@ -159,6 +160,7 @@ widget_ids! { sfx_volume_text, audio_device_list, audio_device_text, + // hotbar_title, bar_numbers_title, show_bar_numbers_none_button, @@ -167,18 +169,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 +199,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, @@ -261,6 +266,7 @@ pub enum Event { ToggleTips(bool), ToggleBarNumbers(BarNumbers), ToggleShortcutNumbers(ShortcutNumbers), + BuffPosition(BuffPosition), ChangeTab(SettingsTab), Close, AdjustMousePan(u32), @@ -829,11 +835,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 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..8b4396a094 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::Bar, bar_numbers: BarNumbers::Values, ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()), free_look_behavior: PressBehavior::Toggle,