From af72c46c026b2dda320be65d2c6ae3f80fe52589 Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Fri, 7 Aug 2020 01:53:49 +0300 Subject: [PATCH 01/71] change treeculler crate from git to published version --- Cargo.lock | 3 ++- voxygen/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0d9177941..9761c53000 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4416,7 +4416,8 @@ dependencies = [ [[package]] name = "treeculler" version = "0.1.0" -source = "git+https://gitlab.com/yusdacra/treeculler.git#efcf5283cf386117a7e654abdaa45ef664a08e42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa14b9f5cd7d513bab5accebe8f403df8b1ac22302cac549a6ac99c0a007c84a" dependencies = [ "num-traits", "vek", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index dde2880348..e8ff69392e 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -59,7 +59,7 @@ directories-next = "1.0.1" num = "0.2" backtrace = "0.3.40" rand = "0.7" -treeculler = { git = "https://gitlab.com/yusdacra/treeculler.git" } +treeculler = "0.1.0" rodio = { version = "0.11", default-features = false, features = ["wav", "vorbis"] } cpal = "0.11" crossbeam = "=0.7.2" From 599a9a76fcef21ac68083ad2e8d975ab067ea4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=A4rtens?= Date: Fri, 7 Aug 2020 01:56:05 +0200 Subject: [PATCH 02/71] add a translation test that verifys that all language RON files are parseable (without any git involved) --- assets/voxygen/i18n/PL.ron | 6 ++++- assets/voxygen/i18n/sv.ron | 13 ++++++++++- assets/voxygen/i18n/zh_TW.ron | 6 ++++- voxygen/src/i18n.rs | 43 +++++++++++++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/assets/voxygen/i18n/PL.ron b/assets/voxygen/i18n/PL.ron index 91619b7662..cff1730661 100644 --- a/assets/voxygen/i18n/PL.ron +++ b/assets/voxygen/i18n/PL.ron @@ -388,5 +388,9 @@ Siła woli "esc_menu.quit_game": "Opuść gre", /// End Escape Menu Section /// Koniec sekcji Menu pauzy - } + }, + + vector_map: { + + }, ) diff --git a/assets/voxygen/i18n/sv.ron b/assets/voxygen/i18n/sv.ron index ccd0ef6256..f074769680 100644 --- a/assets/voxygen/i18n/sv.ron +++ b/assets/voxygen/i18n/sv.ron @@ -17,6 +17,13 @@ VoxygenLocalization( language_identifier: "sv", ), convert_utf8_to_ascii: false, + // Make sure that fonts contain all swedisch characters + fonts: { + "opensans": Font ( + asset_key: "voxygen.font.OpenSans-Regular", + scale_ratio: 1.0, + ), + }, string_map: { /// Start Common section // Texts used in multiple locations with the same formatting @@ -332,5 +339,9 @@ Willpower "esc_menu.logout": "Logout", "esc_menu.quit_game": "Quit Game", /// End Escape Menu Section - } + }, + + vector_map: { + + }, ) diff --git a/assets/voxygen/i18n/zh_TW.ron b/assets/voxygen/i18n/zh_TW.ron index ebe00de4b1..57fef4922b 100644 --- a/assets/voxygen/i18n/zh_TW.ron +++ b/assets/voxygen/i18n/zh_TW.ron @@ -377,5 +377,9 @@ Veloren 半夜會特別暗。 "esc_menu.logout": "登出", "esc_menu.quit_game": "退出遊戲", /// End Escape Menu Section - } + }, + + vector_map: { + + }, ) diff --git a/voxygen/src/i18n.rs b/voxygen/src/i18n.rs index 9858ff74c0..2558adaaf2 100644 --- a/voxygen/src/i18n.rs +++ b/voxygen/src/i18n.rs @@ -180,7 +180,7 @@ pub fn i18n_asset_key(language_id: &str) -> String { "voxygen.i18n.".to_string() mod tests { use super::VoxygenLocalization; use git2::Repository; - use ron::de::from_bytes; + use ron::de::{from_bytes, from_reader}; use std::{ collections::{HashMap, HashSet}, fs, @@ -325,10 +325,49 @@ mod tests { keys } + // Test to verify all languages that they are VALID and loadable, without + // need of git just on the local assets folder + #[test] + fn verify_all_localizations() { + // Generate paths + let i18n_asset_path = Path::new("assets/voxygen/i18n/"); + let en_i18n_path = i18n_asset_path.join("en.ron"); + let root_dir = std::env::current_dir() + .map(|p| p.parent().expect("").to_owned()) + .unwrap(); + assert!( + root_dir.join(&en_i18n_path).is_file(), + "en reference files doesn't exist, something is wrong!" + ); + let i18n_files = i18n_files(&root_dir.join(i18n_asset_path)); + // This simple check ONLY guarantees that an arbitrary minimum of translation + // files exists. It's just to notice unintentional deletion of all + // files, or modifying the paths. In case you want to delete all + // language you have to adjust this number: + assert!( + i18n_files.len() > 5, + "have less than 5 translation files, arbitrary minimum check failed. Maybe the i18n \ + folder is empty?" + ); + for path in i18n_files { + let f = fs::File::open(&path).expect("Failed opening file"); + let _: VoxygenLocalization = match from_reader(f) { + Ok(v) => v, + Err(e) => { + panic!( + "Could not parse {} RON file, error: {}", + path.to_string_lossy(), + e + ); + }, + }; + } + } + + // Test to verify all languages and print missing and faulty localisation #[test] #[ignore] #[allow(clippy::expect_fun_call)] - /// Test to verify all languages and print missing and faulty localisation fn test_all_localizations() { // Generate paths let i18n_asset_path = Path::new("assets/voxygen/i18n/"); From 54c08babac9c5bf892c3c976865dd7b58542a9ef Mon Sep 17 00:00:00 2001 From: Varpie Date: Fri, 7 Aug 2020 21:05:47 +0000 Subject: [PATCH 03/71] Fixing various obsolete texts and a few typos, uniform pronoun (tu->vous) and French style punctuation --- assets/voxygen/i18n/en.ron | 2 +- assets/voxygen/i18n/fr_FR.ron | 309 +++++++++++++++++----------------- 2 files changed, 158 insertions(+), 153 deletions(-) diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 2aefbb6dcc..0d4c6c2f9d 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -331,7 +331,7 @@ magically infused items?"#, "gameinput.primary": "Basic Attack", "gameinput.secondary": "Secondary Attack/Block/Aim", - "gameinput.slot1": "Hotbar Slot 1", + "gameinput.slot1": "Hotbar Slot 1", "gameinput.slot2": "Hotbar Slot 2", "gameinput.slot3": "Hotbar Slot 3", "gameinput.slot4": "Hotbar Slot 4", diff --git a/assets/voxygen/i18n/fr_FR.ron b/assets/voxygen/i18n/fr_FR.ron index 478b6cf011..0a0ea98d9d 100644 --- a/assets/voxygen/i18n/fr_FR.ron +++ b/assets/voxygen/i18n/fr_FR.ron @@ -44,10 +44,10 @@ VoxygenLocalization( "common.resume": "Reprendre", "common.characters": "Personages", "common.close": "Fermer", - "common.create": "Créer", - "common.back": "Retour", "common.yes": "Oui", "common.no": "Non", + "common.back": "Retour", + "common.create": "Créer", "common.okay": "Compris", "common.accept": "Accepter", "common.disclaimer": "Avertissement", @@ -56,6 +56,12 @@ VoxygenLocalization( "common.error": "Erreur", "common.fatal_error": "Erreur Fatale", + // Message when connection to the server is lost + "common.connection_lost": r#"Connexion perdue ! +Le serveur a-il redémarré? +Le client est-il à jour?"#, + + "common.species.orc": "Orc", "common.species.human": "Humain", "common.species.dwarf": "Nain", @@ -69,27 +75,28 @@ VoxygenLocalization( "common.weapons.bow": "Arc", "common.weapons.hammer": "Marteau", - // Message when connection to the server is lost - "common.connection_lost": r#"Connexion perdue! -Le serveur a-il redémarré? -Le client est-il à jour?"#, // Main screen texts "main.connecting": "Connexion", "main.creating_world": "Création du monde", + "main.tip": "Astuce:", + + // Annonce de bienvenui qui apparaît la première fois que Veloren est lancé "main.notice": r#"Bienvenue dans la version alpha de Veloren! Avant de commencer à vous amuser, merci de garder les choses suivantes en tête: - Il s'agit d'une version alpha très jeune. Attendez-vous à des bugs, un gameplay non terminé, des mécaniques non peaufinées et des fonctionalités manquantes. -- Si vous avez des retours constructifs ou avez detecté un bug, vous pouvez nous contacter via Reddit, GitLab ou notre communauté Discord. -- Veloren est un logiciel open-source sour licence GPL3. Cela signifit que vous êtes libre de jouer, modfier et redistribuer le jeu comme il vous semble (licence contaminante sous GPL 3 pour toute modification) -- Veloren est un projet communautaire à but non-lucratif développé par des bénévolles. -Si vous apprecier ce jeu, vous êtes les bienvenues pour rejoindre les équipes de développement ou d'artistes! -- Le genre 'Voxel RPG' est un genre à part entiere. Les FPS étaient appelé Doom-like. De la même façon, nous essayons de construire un genre à part entière. Ce jeu n'est pas un clone et ses mechaniques changeront au cours du developpement. -Merci d'avoir pris le temps de lire cette notice, nous esperons que vous apprecierez le jeu! +- Si vous avez des retours constructifs ou avez detecté un bug, vous pouvez nous contacter via Reddit, GitLab ou le serveur de notre communauté Discord. + +- Veloren est un logiciel open-source sour licence GPL3. Cela signifit que vous êtes libre de jouer, modfier et redistribuer le jeu comme il vous semble (licence contaminante sous GPL 3 pour toute modification) + +- Veloren est un projet communautaire à but non-lucratif développé par des bénévoles. +Si vous apprecier ce jeu, vous êtes les bienvenues pour rejoindre les équipes de développement ou d'artistes! + +Merci d'avoir pris le temps de lire cette annonce, nous espérons que vous apprecierez le jeu! ~ L'équipe de Veloren"#, @@ -107,7 +114,7 @@ https://account.veloren.net."#, "main.login.server_full": "Serveur plein", "main.login.untrusted_auth_server": "Le serveur d'authentification n'est pas de confiance", "main.login.outdated_client_or_server": "ServeurPasContent: Les versions sont probablement incompatibles, verifiez les mises à jour.", - "main.login.timeout": "DélaiEcoulé: Le serveur n'a pas repondu à temps. (Surchage ou Problèmes réseau).", + "main.login.timeout": "DélaiEcoulé: Le serveur n'a pas repondu à temps. (Surchage ou problèmes réseau).", "main.login.server_shut_down": "Extinction du Serveur", "main.login.already_logged_in": "Vous êtes déjà connecté à ce serveur.", "main.login.network_error": "Problème Réseau", @@ -115,10 +122,10 @@ https://account.veloren.net."#, "main.login.invalid_character": "Le personnage sélectionné n'est pas valide", "main.login.client_crashed": "Le client a planté", "main.login.not_on_whitelist": "Vous devez être ajouté à la liste blanche par un Admin pour pouvoir entrer", - "main.tip": "Astuce:", /// End Main screen section + ///Début section Hud "hud.do_not_show_on_startup": "Ne pas afficher au démarage", "hud.show_tips": "Voir les astuces", @@ -126,11 +133,10 @@ https://account.veloren.net."#, "hud.you_died": "Vous êtes mort", "hud.waypoint_saved": "Point de Repère Sauvegardé", - "hud.press_key_to_show_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles", "hud.press_key_to_show_debug_info_fmt": "Appuyer sur {key} pour afficher les informations de debogage", - "hud.press_key_to_toggle_debug_info_fmt": "Appuyer sur {key} pour activer les informations de debogage", "hud.press_key_to_toggle_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles", + "hud.press_key_to_toggle_debug_info_fmt": "Appuyer sur {key} pour activer les informations de debogage", // Sorties Tchat "hud.chat.online_msg": "[{name}] est maintenant en ligne.", @@ -147,7 +153,6 @@ https://account.veloren.net."#, // Respawn message "hud.press_key_to_respawn": r#"Appuyez sur {key} pour réapparaitre au dernier feu de camp visité"#, - /// Welcome message "hud.welcome": r#"Bienvenue dans la version Alpha de Veloren! @@ -155,16 +160,12 @@ https://account.veloren.net."#, Quelques astuces avant de démarrer: -POINT LE PLUS IMPORTANT: Pour configurer votre point de resurection tapez -/waypoint dans le chat. (y-compris si vous être déjà mort!) - - Appuyer sur F1 pour voir les commandes disponibles. Tapez /help dans le chat pour voir les commandes du chat. -Des coffres et autres objets sont disposés aléatoirement dans le monde! +Des coffres et autres objets sont disposés aléatoirement dans le monde ! Utilisez le click droit pour le collecter. @@ -174,15 +175,18 @@ Double cliquez sur les éléments de votre sac pour les utiliser ou les équiper Jettez-les en cliquant sur un element puis en cliquant en dehors du sac. + Les nuits peuvent être très sombre à Veloren. -Allumez votre lanterne en tapant /lantern dans le chat. +Allumez votre lanterne en appuyant sur 'G'. + + +Vous souhaitez libérer votre souris pour fermer cette fenêtre? Appuyez sur TAB! -Vous souhaitez libérer votre souris pour fermer cette fenêtre? Tapez sur TAB! Profitez de votre séjour dans le monde de Veloren."#, -"hud.temp_quest_headline": r#"S'il vous plaît, aidez nous voyageur!"#, +"hud.temp_quest_headline": r#"S'il vous plaît, aidez-nous voyageur!"#, "hud.temp_quest_text": r#"Des donjons remplis de cultistes malfaisants sont apparus tout autour de nos paisibles villages! @@ -204,7 +208,7 @@ objets magiques ?"#, "hud.bag.stats": "Attributs", "hud.bag.head": "Tête", "hud.bag.neck": "Cou", - "hud.bag.tabard": "Tabar", + "hud.bag.tabard": "Tabard", "hud.bag.shoulders": "Epaules", "hud.bag.chest": "Torse", "hud.bag.hands": "Mains", @@ -215,17 +219,17 @@ objets magiques ?"#, "hud.bag.legs": "Jambes", "hud.bag.feet": "Pieds", "hud.bag.mainhand": "Main Dominante", - "hud.bag.offhand": "Main Dominée", + "hud.bag.offhand": "Main Secondaire", + // Carte et journal de quetes "hud.map.map_title": "Carte", "hud.map.qlog_title": "Quêtes", - //Paramètres "hud.settings.general": "Général", "hud.settings.none": "Aucun", - "hud.settings.press_behavior.toggle": "Activer/Desactiver", + "hud.settings.press_behavior.toggle": "Activer/Désactiver", "hud.settings.press_behavior.hold": "Maintenir", "hud.settings.help_window": "Fenêtre d'aide", "hud.settings.debug_info": "Information de débogage", @@ -237,12 +241,12 @@ objets magiques ?"#, "hud.settings.transparency": "Transparence", "hud.settings.hotbar": "Barre d'action", "hud.settings.toggle_shortcuts": "Activer les raccourcis", - "hud.settings.toggle_bar_experience": "Activer la barre d'experience", + "hud.settings.toggle_bar_experience": "Activer la barre d'expérience", "hud.settings.scrolling_combat_text": "Dégats de combat", - "hud.settings.single_damage_number": "Dégat adversaire (par dégat)", - "hud.settings.cumulated_damage": "Dégat adversaire (cumulé)", - "hud.settings.incoming_damage": "Dégat personnage (par dégat)", - "hud.settings.cumulated_incoming_damage": "Dégat personnage (cumulé)", + "hud.settings.single_damage_number": "Dégats infligés", + "hud.settings.cumulated_damage": "Dégat infligés cumulés", + "hud.settings.incoming_damage": "Dégats reçus", + "hud.settings.cumulated_incoming_damage": "Dégats reçus cumulés", "hud.settings.speech_bubble": "Bulle de dialogue", "hud.settings.speech_bubble_dark_mode": "Bulle de dialogue Mode Sombre", "hud.settings.speech_bubble_icon": "Icône Bulle de dialogue", @@ -251,8 +255,8 @@ objets magiques ?"#, "hud.settings.percentages": "Pourcentages", "hud.settings.chat": "Tchat", "hud.settings.background_transparency": "Transparence du fond", - "hud.settings.chat_character_name": "Nom des personnages dans le tchat", - "hud.settings.loading_tips": "Astuces de chargement", + "hud.settings.chat_character_name": "Noms des personnages dans le tchat", + "hud.settings.loading_tips": "Astuces sur l'écran de chargement", "hud.settings.pan_sensitivity": "Sensibilité de la souris", "hud.settings.zoom_sensitivity": "Sensibilité du zoom", @@ -287,11 +291,11 @@ objets magiques ?"#, "hud.settings.reset_keybinds": "Réinitialiser touches par défaut", "hud.social": "Social", + "hud.social.online": "En ligne", "hud.social.friends": "Amis", - "hud.social.faction": "Faction", - "hud.social.online": "Jeu en ligne", "hud.social.not_yet_available": "Pas encore disponible", - "hud.social.play_online_fmt": "{nb_player} joueurs en ligne", + "hud.social.faction": "Faction", + "hud.social.play_online_fmt": "{nb_player} joueur(s) en ligne", "hud.crafting": "Fabrication", "hud.crafting.recipes": "Recettes", @@ -304,9 +308,11 @@ objets magiques ?"#, "hud.free_look_indicator": "Vue libre active", "hud.auto_walk_indicator": "Marche automatique active", - //Fin Section Hud + /// Fin Section Hud + /// Debut de section GameInput + "gameinput.primary": "Attaque Basique", "gameinput.secondary": "Attaque Secondaire/Bloquer/Viser", "gameinput.slot1": "Emplacement rapide 1", @@ -323,7 +329,7 @@ objets magiques ?"#, "gameinput.togglecursor": "Activer/Desactiver Curseur", "gameinput.help": "Activer/Desactiver Fenêtre d'aide", "gameinput.toggleinterface": "Activer/Desactiver Interface", - "gameinput.toggledebug": "Activer/Desactiver IPS et Infos Debogage", + "gameinput.toggledebug": "Activer/Desactiver FPS et Infos Debogage", "gameinput.screenshot": "Prendre une capture d'écran", "gameinput.toggleingameui": "Activer/Desactiver Noms de joueurs", "gameinput.fullscreen": "Activer/Desactiver Plein Ecran", @@ -341,7 +347,7 @@ objets magiques ?"#, "gameinput.mount": "Monture", "gameinput.enter": "Entrer", "gameinput.command": "Commande", - "gameinput.escape": "Fuir", + "gameinput.escape": "Menu principal/Fermer menu", "gameinput.map": "Carte", "gameinput.bag": "Sac", "gameinput.social": "Social", @@ -358,43 +364,41 @@ objets magiques ?"#, /// End GameInput section - /// Debut Section Menu Start Quitter - "esc_menu.quit_game": "Quitter le jeu", - "esc_menu.logout": "Se déconnecter", - - /// Fin Section Menu Start Quitter /// Debut de la section Création du personnage - "char_selection.accessories": "Accessoires", - "char_selection.beard": "Barbe", + "char_selection.loading_characters": "Chargement des personnages...", + "char_selection.delete_permanently": "Supprimer définitivement ce personnage ?", + "char_selection.deleting_character": "Suppression du personnage...", + "char_selection.change_server": "Changer de serveur", + "char_selection.enter_world": "Entrer dans le monde", + "char_selection.logout": "Se déconnecter", "char_selection.create_new_charater": "Créer un nouveau personnage", "char_selection.creating_character": "Création du personnage...", - "char_selection.character_creation": "Création de personnages", - "char_selection.create_info_name": "Votre personnage doit avoir un prénom !", - "char_selection.deleting_character": "Suppression du personnage...", - "char_selection.enter_world": "Entrer dans le monde", - "char_selection.eyebrows": "Sourcils", - "char_selection.eye_color": "Couleur des yeux", - "char_selection.eyeshape": "Forme des yeux", - "char_selection.hair_style": "Coupe de cheveux", - "char_selection.hair_color": "Couleur des cheveux", + "char_selection.character_creation": "Création de personnage", + "char_selection.human_default": "Humain par défault", "char_selection.level_fmt": "Niveau {level_nb}", - "char_selection.logout": "Se déconnecter", - "char_selection.loading_characters": "Chargement des personnages...", - "char_selection.plains_of_uncertainty": "Plaines de l'incertitude", - "char_selection.skin": "Couleur de peau", - "char_selection.uncanny_valley": "Vallée dérangeante", - "char_selection.change_server": "Changer de serveur", - "char_selection.delete_permanently": "Supprimer définitivement ce personnage ?", + "char_selection.uncanny_valley": "Région sauvage", + "char_selection.plains_of_uncertainty": "Plaines de l'Incertitude", + "char_selection.beard": "Barbe", + "char_selection.hair_style": "Coupe de cheveux", + "char_selection.hair_color": "Couleur des cheveux", + "char_selection.eye_color": "Couleur des yeux", + "char_selection.skin": "Couleur de la peau", + "char_selection.eyeshape": "Forme des yeux", + "char_selection.accessories": "Accessoires", + "char_selection.create_info_name": "Votre personnage doit avoir un prénom !", + /// Fin de la section Création du personnage + /// Start character window section "character_window.character_name": "Personnage", + // Character stats "character_window.character_stats": r#"Endurance Force -Dexterité +Volonté Protection "#, @@ -408,105 +412,106 @@ Protection }, + vector_map: { "loading.tips": [ - "Appuie sur 'G' pour allumer ta lanterne.", - "Appuie sur 'F1' pour voir les raccourcis clavier par défaut.", - "Tu peux écrire /say ou /s pour discuter aux joueurs directement à côté toi.", - "Tu peux écrire /region ou /r pour discuter avec les joueurs situés à quelques centaines de blocs de toi.", - "Pour envoyer un message privé, écrit /tell suivi par un nom de joueur puis ton message.", + "Appuiez sur 'G' pour allumer ta lanterne.", + "Appuiez sur 'F1' pour voir les raccourcis clavier par défaut.", + "Vous pouvez taper /say ou /s pour discuter aux joueurs directement à côté toi.", + "Vous pouvez taper /region ou /r pour discuter avec les joueurs situés à quelques centaines de blocs de toi.", + "Pour envoyer un message privé, tapez /tell suivi par un nom de joueur puis votre message.", "Des PNJs avec le même niveau peuvent varier en difficulté.", - "Regarde le sol pour trouver de la nourriture, des coffres et d'autres butins!", - "Ton inventaire est rempli de nourriture? Essaie de créer un meilleur repas avec!", - "Tu cherches une activité? Les donjons sont marqués avec des points marron sur la carte!", - "N'oublie pas d'ajuster les graphiques pour ton système. Appuie sur 'N' pour ouvrir les paramètres.", - "Jouer à plusieurs est amusant! Appuie sur 'O' pour voir qui est en ligne.", - "Un PNJ avec une tête-de-mort sous sa barre de vie est plus puissant que toi.", - "Appuie sur 'J' pour danser. C'est la fête!", - "Appuie sur 'L-Shift'pour ouvrir ton deltaplane et conquérir les cieux.", + "Regardez le sol pour trouver de la nourriture, des coffres et d'autres butins!", + "Votre inventaire est rempli de nourriture? Essayez de créer un meilleur repas avec!", + "Vous cherchez une activité? Les donjons sont marqués avec des points marron sur la carte !", + "N'oubliez pas d'ajuster les graphiques pour votre système. Appuyez sur 'N' pour ouvrir les paramètres.", + "Jouer à plusieurs est amusant! Appuyez sur 'O' pour voir qui est en ligne.", + "Un PNJ avec une tête de mort sous sa barre de vie est plus puissant que vous.", + "Appuyez sur 'J' pour danser. C'est la fête !", + "Appuyez sur 'L-Shift'pour ouvrir votre deltaplane et conquérir les cieux.", "Veloren est encore Pre-Alpha. Nous faisons de notre mieux pour l'améliorer chaque jour!", - "Si tu veux te joindre à l'équipe de développement ou juste discuter avec nous, rejoins notre serveur Discord.", + "Si vous voulez vous joindre à l'équipe de développement ou juste discuter avec nous, rejoignez notre serveur Discord.", ], "npc.speech.villager_under_attack": [ - "À l'aide, on m'attaque!", - "À l'aide! On m'attaque!", - "Aïe! On m'attaque!", - "Aïe! On m'attaque! À l'aide!", - "Aidez-moi! On m'attaque!", - "On m'attaque! À l'aide!", - "On m'attaque! Aidez-moi!", - "À l'aide!", - "À l'aide! À l'aide!", - "À l'aide! À l'aide! À l'aide!", - "On m'attaque!", - "AAAHHH! On m'attaque!", - "AAAHHH! On m'attaque! À l'aide!", - "À l'aide! Nous sommes attaqués!", - "À l'aide! Assassin!", - "À l'aide! Il y a un assassin en liberté!", - "À l'aide! On essaie de me tuer!", - "Gardes, on m'attaque!", - "Gardes! On m'attaque!", - "On m'attaque! Gardes!", - "À l'aide! Gardes! On m'attaque!", - "Gardes! Venez vite!", - "Gardes! Gardes!", - "Gardes! Un scélérat m'attaque!", - "Gardes, abattez ce scélérat!", - "Gardes! Il y a un meurtrier!", - "Gardes! Aidez-moi!", - "Vous ne vous en tirerez pas comme ça! Gardes!", - "Monstre!", + "À l'aide, on m'attaque !", + "À l'aide ! On m'attaque !", + "Aïe ! On m'attaque !", + "Aïe ! On m'attaque ! À l'aide !", + "Aidez-moi! On m'attaque !", + "On m'attaque ! À l'aide !", + "On m'attaque ! Aidez-moi !", + "À l'aide !", + "À l'aide ! À l'aide !", + "À l'aide ! À l'aide ! À l'aide !", + "On m'attaque !", + "AAAHHH ! On m'attaque !", + "AAAHHH ! On m'attaque ! À l'aide !", + "À l'aide ! Nous sommes attaqués !", + "À l'aide ! Assassin !", + "À l'aide ! Il y a un assassin en liberté !", + "À l'aide ! On essaie de me tuer !", + "Gardes, on m'attaque !", + "Gardes ! On m'attaque !", + "On m'attaque ! Gardes !", + "À l'aide ! Gardes ! On m'attaque !", + "Gardes ! Venez vite !", + "Gardes ! Gardes !", + "Gardes ! Un scélérat m'attaque !", + "Gardes, abattez ce scélérat !", + "Gardes ! Il y a un meurtrier !", + "Gardes ! Aidez-moi!", + "Vous ne vous en tirerez pas comme ça! Gardes !", + "Monstre !", "Aidez-moi!", - "À l'aide! S'il vous plait!", - "Aïe! Gardes! À l'aide!", + "À l'aide ! S'il vous plait !", + "Aïe ! Gardes ! À l'aide !", "Ils viennent pour moi !", - "À l'aide! À l'aide! Je me fais réprimer!", + "À l'aide ! À l'aide ! Je me fais réprimer !", "Ah, nous voyons maintenant la violence inhérente au système.", "C'est seulement une égratignure.", - "Arrêtez ça!", - "Qu'est ce que je t'ai fait?!", - "S'il te plaît arrête de m'attaquer!", - "Hé! Regardez où vous pointez cette chose!", - "Misérable, allez-vous-en!", - "Arrêtez! Partez! Arrêtez!", - "Vous m'avez ennervé!", - "Oi! Qui croyez-vous être?!", - "J'aurais votre tête pour ça!", - "Stoppez s'il vous plaît! Je ne transporte rien de valeur!", - "Je vais appeler mon frère, il est plus grand que moi!", - "Nooon, Je vais le dire à ma mère!", - "Soyez maudit!", + "Arrêtez ça !", + "Qu'est ce que je vous ai fait ?!", + "S'il vous plaît arrêtez de m'attaquer !", + "Hé! Regardez où vous pointez cette chose !", + "Misérable, allez-vous-en !", + "Arrêtez ! Partez ! Arrêtez !", + "Vous m'avez ennervé !", + "Oi ! Qui croyez-vous être ?!", + "J'aurais votre tête pour ça !", + "Arrêtez, s'il vous plaît ! Je ne transporte rien de valeur !", + "Je vais appeler mon frère, il est plus grand que moi !", + "Nooon, Je vais le dire à ma mère !", + "Soyez maudit !", "Ne faites pas ça.", - "Ce n'était pas très gentil!", - "Ton arme fonctionne, tu peux la ranger maintenant!", - "Épargnez-moi!", - "Pitié, J'ai une famille!", - "Je suis trop jeune pour mourrir!", - "On peut en parler?", - "La violence n'est jamais la solution!", + "Ce n'était pas très gentil !", + "Ton arme fonctionne, tu peux la ranger maintenant !", + "Épargnez-moi !", + "Pitié, J'ai une famille !", + "Je suis trop jeune pour mourrir !", + "On peut en parler ?", + "La violence n'est jamais la solution !", "Aujourd'hui est une très mauvaise journée...", - "Hé, ça fait mal!", - "Aïe!", - "Quelle impolitesse!", - "Stop, je vous en prie!", - "Que la peste vous emporte!", + "Hé, ça fait mal !", + "Aïe !", + "Quelle impolitesse !", + "Stop, je vous en prie !", + "Que la peste vous emporte !", "Ce n'est pas amusant.", - "Comment osez-vous?!", - "Vous allez payer!", - "Continue et tu vas le regretter!", - "Ne m'obligez pas à vous faire du mal!", - "Il doit y avoir erreur!", - "Vous n'avez pas besoin de faire ça!", - "Fuyez, monstre!", - "Ça fait vraiment mal!", - "Pourquoi faites-vous cela?", - "Par les esprits, cessez!", - "Vous devez m'avoir confondu avec quelqu'un d'autre!", - "Je ne mérite pas cela!", + "Comment osez-vous ?!", + "Vous allez payer !", + "Continue et tu vas le regretter !", + "Ne m'obligez pas à vous faire du mal !", + "Il doit y avoir erreur !", + "Vous n'avez pas besoin de faire ça !", + "Fuyez, monstre !", + "Ça fait vraiment mal !", + "Pourquoi faites-vous cela ?", + "Par les esprits, cessez !", + "Vous devez m'avoir confondu avec quelqu'un d'autre !", + "Je ne mérite pas cela !", "Ne faites plus cela.", - "Gardes, jetez ce monstre dans le lac!", - "Je vais t'envoyer ma tarrasque!", + "Gardes, jetez ce monstre dans le lac !", + "Je vais t'envoyer ma tarrasque !", ], } -) \ No newline at end of file +) From 1741384d008c865b126de80a667cde5575f66bf8 Mon Sep 17 00:00:00 2001 From: Imbris Date: Fri, 24 Apr 2020 04:02:47 -0400 Subject: [PATCH 04/71] Add entity targeting --- voxygen/src/hud/mod.rs | 5 +- voxygen/src/scene/figure/mod.rs | 15 +++- voxygen/src/scene/mod.rs | 1 + voxygen/src/session.rs | 141 ++++++++++++++++++++++++-------- 4 files changed, 121 insertions(+), 41 deletions(-) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 6cad71c471..39edeff12e 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -242,6 +242,7 @@ pub struct DebugInfo { pub struct HudInfo { pub is_aiming: bool, pub is_first_person: bool, + pub target_entity: Option, } pub enum Event { @@ -1045,7 +1046,9 @@ impl Hud { &uids, ) .join() - .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead) + .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead + && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity)) + ) // Don't show outside a certain range .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { pos.0.distance_squared(player_pos) diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 16fb4d1bcd..980433f024 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -22,8 +22,8 @@ use anim::{ }; use common::{ comp::{ - item::ItemKind, Body, CharacterState, Last, LightAnimation, LightEmitter, Loadout, Ori, - PhysicsState, Pos, Scale, Stats, Vel, + item::ItemKind, Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout, + Ori, PhysicsState, Pos, Scale, Stats, Vel, }, state::{DeltaTime, State}, states::triple_strike, @@ -192,7 +192,6 @@ impl FigureMgr { .read_storage::() .get(scene_data.player_entity) .map_or(Vec3::zero(), |pos| pos.0); - for ( i, ( @@ -207,6 +206,7 @@ impl FigureMgr { physics, stats, loadout, + item, ), ) in ( &ecs.entities(), @@ -220,6 +220,7 @@ impl FigureMgr { &ecs.read_storage::(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() .enumerate() @@ -519,7 +520,13 @@ impl FigureMgr { (c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32 }) }) - .unwrap_or(Rgba::broadcast(1.0)); + .unwrap_or(Rgba::broadcast(1.0)) + // Highlight targeted collectible entities + * if item.is_some() && scene_data.target_entity.map_or(false, |e| e == entity) { + Rgba::new(2.0, 2.0, 2.0, 1.0) + } else { + Rgba::one() + }; let scale = scale.map(|s| s.0).unwrap_or(1.0); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 5f449fe15c..4c18a92207 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -70,6 +70,7 @@ pub struct Scene { pub struct SceneData<'a> { pub state: &'a State, pub player_entity: specs::Entity, + pub target_entity: Option, pub loaded_distance: f32, pub view_distance: u32, pub tick: u64, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 00bffa5338..578dc9b0fa 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -208,18 +208,6 @@ impl PlayState for SessionState { view_mat, cam_pos, .. } = self.scene.camera().dependents(); - // Choose a spot above the player's head for item distance checks - let player_pos = match self - .client - .borrow() - .state() - .read_storage::() - .get(self.client.borrow().entity()) - { - Some(pos) => pos.0 + (Vec3::unit_z() * 2.0), - _ => cam_pos, // Should never happen, but a safe fallback - }; - let (is_aiming, aim_dir_offset) = { let client = self.client.borrow(); let is_aiming = client @@ -243,30 +231,10 @@ impl PlayState for SessionState { let cam_dir: Vec3 = Vec3::from(view_mat.inverted() * -Vec4::unit_z()); // Check to see whether we're aiming at anything - let (build_pos, select_pos) = { - let client = self.client.borrow(); - let terrain = client.state().terrain(); - - let cam_ray = terrain - .ray(cam_pos, cam_pos + cam_dir * 100.0) - .until(|block| block.is_tangible()) - .cast(); - - let cam_dist = cam_ray.0; - - match cam_ray.1 { - Ok(Some(_)) - if player_pos.distance_squared(cam_pos + cam_dir * cam_dist) - <= MAX_PICKUP_RANGE_SQR => - { - ( - Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)), - Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)), - ) - }, - _ => (None, None), - } - }; + let (build_pos, select_pos, target_entity) = + under_cursor(&self.client.borrow(), cam_pos, cam_dir); + // Throw out distance info, it will be useful in the future + let target_entity = target_entity.map(|x| x.0); let can_build = self .client @@ -708,6 +676,7 @@ impl PlayState for SessionState { self.scene.camera().get_mode(), camera::CameraMode::FirstPerson ), + target_entity, }, ); @@ -979,6 +948,7 @@ impl PlayState for SessionState { let scene_data = SceneData { state: client.state(), player_entity: client.entity(), + target_entity, loaded_distance: client.loaded_distance(), view_distance: client.view_distance().unwrap_or(1), tick: client.get_tick(), @@ -1055,3 +1025,102 @@ impl PlayState for SessionState { self.hud.render(renderer, self.scene.globals()); } } + +/// Max distance an entity can be "targeted" +const MAX_TARGET_RANGE: f32 = 30.0; +/// Calculate what the cursor is pointing at within the 3d scene +fn under_cursor( + client: &Client, + cam_pos: Vec3, + cam_dir: Vec3, +) -> ( + Option>, + Option>, + Option<(specs::Entity, f32)>, +) { + // Choose a spot above the player's head for item distance checks + let player_entity = client.entity(); + let player_pos = match client + .state() + .read_storage::() + .get(player_entity) + { + Some(pos) => pos.0 + (Vec3::unit_z() * 2.0), + _ => cam_pos, // Should never happen, but a safe fallback + }; + let terrain = client.state().terrain(); + + let cam_ray = terrain + .ray(cam_pos, cam_pos + cam_dir * 100.0) + .until(|block| block.is_tangible()) + .cast(); + + let cam_dist = cam_ray.0; + + // The ray hit something, is it within range? + let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if + player_pos.distance_squared(cam_pos + cam_dir * cam_dist) + <= MAX_PICKUP_RANGE_SQR) + { + ( + Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)), + Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)), + ) + } else { + (None, None) + }; + + // See if ray hits entities + // Currently treated as spheres + let ecs = client.state().ecs(); + // Don't cast through blocks + // Could check for intersection with entity from last frame to narrow this down + let cast_dist = if let Ok(Some(_)) = cam_ray.1 { + cam_dist.min(MAX_TARGET_RANGE) + } else { + MAX_TARGET_RANGE + }; + + // Need to raycast by distance to cam + // But also filter out by distance to the player (but this only needs to be done + // on final result) + let mut nearby = ( + &ecs.entities(), + &ecs.read_storage::(), + ecs.read_storage::().maybe(), + &ecs.read_storage::() + ) + .join() + .filter(|(e, _, _, _)| *e != player_entity) + .map(|(e, p, s, b)| { + const RADIUS_SCALE: f32 = 1.2; + let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; + // Move position up from the feet + let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); + let dist_sqr = pos.distance_squared(cam_pos); + (e, pos, radius, dist_sqr) + }) + // Ignore entities intersecting the camera + .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2)) + .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2)) + .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r)) + .collect::>(); + nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap()); + + let seg_ray = LineSegment3 { + start: cam_pos, + end: cam_pos + cam_dir * cam_dist, + }; + // TODO: fuzzy borders + let target_entity = nearby + .iter() + .map(|(e, p, r, _)| (e, *p, r)) + .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2)) + .and_then(|(e, p, r)| { + let dist_to_player = p.distance(player_pos); + (dist_to_player - r < MAX_TARGET_RANGE).then_some((*e, dist_to_player)) + }); + + // TODO: consider setting build/select to None when targeting an entity + (build_pos, select_pos, target_entity) +} From 10c3d0146652deaaffcf52113a77c81964eaf234 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 26 Apr 2020 13:03:19 -0400 Subject: [PATCH 05/71] Add basic group functionality (no voxygen wiring yet) --- Cargo.lock | 1 + client/src/lib.rs | 100 ++++++- common/Cargo.toml | 3 +- common/src/comp/agent.rs | 7 +- common/src/comp/controller.rs | 11 + common/src/comp/group.rs | 392 +++++++++++++++++++++++++++ common/src/comp/mod.rs | 11 +- common/src/event.rs | 1 + common/src/msg/ecs_packet.rs | 10 +- common/src/msg/server.rs | 2 + common/src/state.rs | 8 +- common/src/sys/agent.rs | 34 ++- common/src/sys/combat.rs | 58 ++-- common/src/sys/controller.rs | 3 + server/src/client.rs | 1 + server/src/cmd.rs | 44 ++- server/src/events/entity_creation.rs | 16 +- server/src/events/group_manip.rs | 304 +++++++++++++++++++++ server/src/events/inventory_manip.rs | 30 +- server/src/events/mod.rs | 3 + server/src/events/player.rs | 33 ++- server/src/lib.rs | 1 + server/src/state_ext.rs | 26 +- server/src/sys/sentinel.rs | 16 +- 24 files changed, 1028 insertions(+), 87 deletions(-) create mode 100644 common/src/comp/group.rs create mode 100644 server/src/events/group_manip.rs diff --git a/Cargo.lock b/Cargo.lock index b0d9177941..709ab9ea4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4638,6 +4638,7 @@ dependencies = [ "ron", "serde", "serde_json", + "slab", "specs", "specs-idvs", "sum_type", diff --git a/client/src/lib.rs b/client/src/lib.rs index 1603618a92..298c9bef15 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -17,8 +17,8 @@ use byteorder::{ByteOrder, LittleEndian}; use common::{ character::CharacterItem, comp::{ - self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip, - InventoryUpdateEvent, + self, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, + InventoryManip, InventoryUpdateEvent, }, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, @@ -74,11 +74,15 @@ pub struct Client { pub server_info: ServerInfo, pub world_map: (Arc, Vec2), pub player_list: HashMap, + pub group_members: HashSet, pub character_list: CharacterList, pub active_character_id: Option, recipe_book: RecipeBook, available_recipes: HashSet, + group_invite: Option, + group_leader: Option, + _network: Network, participant: Option, singleton_stream: Stream, @@ -208,11 +212,15 @@ impl Client { server_info, world_map, player_list: HashMap::new(), + group_members: HashSet::new(), character_list: CharacterList::default(), active_character_id: None, recipe_book, available_recipes: HashSet::default(), + group_invite: None, + group_leader: None, + _network: network, participant: Some(participant), singleton_stream: stream, @@ -424,6 +432,53 @@ impl Client { .unwrap(); } + pub fn group_invite(&self) -> Option { self.group_invite } + + pub fn group_leader(&self) -> Option { self.group_leader } + + pub fn accept_group_invite(&mut self) { + // Clear invite + self.group_invite.take(); + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Accept, + ))).unwrap(); + } + + pub fn reject_group_invite(&mut self) { + // Clear invite + self.group_invite.take(); + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Reject, + ))).unwrap(); + } + + pub fn leave_group(&mut self) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Leave, + ))).unwrap(); + } + + pub fn kick_from_group(&mut self, entity: specs::Entity) { + if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Kick(uid), + ))).unwrap(); + } + } + + pub fn assign_group_leader(&mut self, entity: specs::Entity) { + if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::AssignLeader(uid), + ))).unwrap(); + } + } + pub fn is_mounted(&self) -> bool { self.state .ecs() @@ -935,7 +990,46 @@ impl Client { ); } }, - + ServerMsg::GroupUpdate(change_notification) => { + use comp::group::ChangeNotification::*; + // Note: we use a hashmap since this would not work with entities outside + // the view distance + match change_notification { + Added(uid) => { + warn!("message to add: {}", uid); + if !self.group_members.insert(uid) { + warn!( + "Received msg to add uid {} to the group members but they \ + were already there", + uid + ); + } + }, + Removed(uid) => { + if !self.group_members.remove(&uid) { + warn!( + "Received msg to remove uid {} from group members but by \ + they weren't in there!", + uid + ); + } + }, + NewLeader(leader) => { + self.group_leader = Some(leader); + }, + NewGroup { leader, members } => { + self.group_leader = Some(leader); + self.group_members = members.into_iter().collect(); + }, + NoGroup => { + self.group_leader = None; + self.group_members = HashSet::new(); + } + } + }, + ServerMsg::GroupInvite(uid) => { + self.group_invite = Some(uid); + }, ServerMsg::Ping => { self.singleton_stream.send(ClientMsg::Pong)?; }, diff --git a/common/Cargo.toml b/common/Cargo.toml index 6a7d2e8cc5..528fd962be 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -29,7 +29,8 @@ crossbeam = "0.7" notify = "5.0.0-pre.3" indexmap = "1.3.0" sum_type = "0.2.0" -authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" } +authc = { git = "https://gitlab.com/veloren/auth.git", rev = "223a4097f7ebc8d451936dccb5e6517194bbf086" } +slab = "0.4.2" [dev-dependencies] criterion = "0.3" diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index d8bf6336db..41eb83a1cd 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -1,10 +1,9 @@ use crate::{path::Chaser, sync::Uid}; -use serde::{Deserialize, Serialize}; -use specs::{Component, Entity as EcsEntity, FlaggedStorage}; +use specs::{Component, Entity as EcsEntity}; use specs_idvs::IdvStorage; use vek::*; -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Alignment { /// Wild animals and gentle giants Wild, @@ -52,7 +51,7 @@ impl Alignment { } impl Component for Alignment { - type Storage = FlaggedStorage>; + type Storage = IdvStorage; } #[derive(Clone, Debug, Default)] diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 01942d68f1..517a4e705d 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -18,12 +18,23 @@ pub enum InventoryManip { CraftRecipe(String), } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum GroupManip { + Invite(Uid), + Accept, + Reject, + Leave, + Kick(Uid), + AssignLeader(Uid), +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum ControlEvent { ToggleLantern, Mount(Uid), Unmount, InventoryManip(InventoryManip), + GroupManip(GroupManip), Respawn, } diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs new file mode 100644 index 0000000000..339919a9dc --- /dev/null +++ b/common/src/comp/group.rs @@ -0,0 +1,392 @@ +use crate::{comp::Alignment, sync::Uid}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; +use slab::Slab; +use specs::{Component, FlaggedStorage, Join}; +use specs_idvs::IdvStorage; +use tracing::{error, warn}; + +// Primitive group system +// Shortcomings include: +// - no support for more complex group structures +// - lack of complex enemy npc integration +// - relies on careful management of groups to maintain a valid state +// - clients don't know what entities are their pets +// - the possesion rod could probably wreck this + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Group(u32); + +// TODO: Hack +// Corresponds to Alignment::Enemy +pub const ENEMY: Group = Group(u32::MAX); +// Corresponds to Alignment::Npc | Alignment::Tame +pub const NPC: Group = Group(u32::MAX - 1); + +impl Component for Group { + type Storage = FlaggedStorage>; +} + +#[derive(Copy, Clone, Debug)] +pub struct GroupInfo { + // TODO: what about enemy groups, either the leader will constantly change because they have to + // be loaded or we create a dummy entity or this needs to be optional + pub leader: specs::Entity, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ChangeNotification { + // :D + Added(E), + // :( + Removed(E), + NewLeader(E), + // Use to put in a group overwriting existing group + NewGroup { leader: E, members: Vec }, + // No longer in a group + NoGroup, +} +// Note: now that we are dipping into uids here consider just using +// ChangeNotification everywhere +// Also note when the same notification is sent to multiple destinations the +// maping might be duplicated effort +impl ChangeNotification { + pub fn try_map(self, f: impl Fn(E) -> Option) -> Option> { + match self { + Self::Added(e) => f(e).map(ChangeNotification::Added), + Self::Removed(e) => f(e).map(ChangeNotification::Removed), + Self::NewLeader(e) => f(e).map(ChangeNotification::NewLeader), + // Note just discards members that fail map + Self::NewGroup { leader, members } => { + f(leader).map(|leader| ChangeNotification::NewGroup { + leader, + members: members.into_iter().filter_map(f).collect(), + }) + }, + Self::NoGroup => Some(ChangeNotification::NoGroup), + } + } +} + +type GroupsMut<'a> = specs::WriteStorage<'a, Group>; +type Groups<'a> = specs::ReadStorage<'a, Group>; +type Alignments<'a> = specs::ReadStorage<'a, Alignment>; +type Uids<'a> = specs::ReadStorage<'a, Uid>; + +#[derive(Debug, Default)] +pub struct GroupManager { + groups: Slab, +} + +// Gather list of pets of the group member + member themselves +// Note: iterating through all entities here could become slow at higher entity +// counts +fn with_pets( + entity: specs::Entity, + uid: Uid, + alignments: &Alignments, + entities: &specs::Entities, +) -> Vec { + let mut list = (entities, alignments) + .join() + .filter_map(|(e, a)| matches!(a, Alignment::Owned(owner) if *owner == uid).then_some(e)) + .collect::>(); + list.push(entity); + list +} + +/// Returns list of current members of a group +pub fn members<'a>( + group: Group, + groups: impl Join + 'a, + entities: &'a specs::Entities, +) -> impl Iterator + 'a { + (entities, groups) + .join() + .filter_map(move |(e, g)| (*g == group).then_some(e)) +} + +// TODO: optimize add/remove for massive NPC groups +impl GroupManager { + pub fn group_info(&self, group: Group) -> Option { + self.groups.get(group.0 as usize).copied() + } + + fn create_group(&mut self, leader: specs::Entity) -> Group { + Group(self.groups.insert(GroupInfo { leader }) as u32) + } + + fn remove_group(&mut self, group: Group) { self.groups.remove(group.0 as usize); } + + // Add someone to a group + // Also used to create new groups + pub fn add_group_member( + &mut self, + leader: specs::Entity, + new_member: specs::Entity, + entities: &specs::Entities, + groups: &mut GroupsMut, + alignments: &Alignments, + uids: &Uids, + mut notifier: impl FnMut(specs::Entity, ChangeNotification), + ) { + // Get uid + let new_member_uid = if let Some(uid) = uids.get(new_member) { + *uid + } else { + error!("Failed to retrieve uid for the new group member"); + return; + }; + + // If new member is a member of a different group remove that + if groups.get(new_member).is_some() { + self.remove_from_group( + new_member, + groups, + alignments, + uids, + entities, + &mut notifier, + ) + } + + let group = match groups.get(leader).copied() { + Some(id) + if self + .group_info(id) + .map(|info| info.leader == leader) + .unwrap_or(false) => + { + Some(id) + }, + // Member of an existing group can't be a leader + // If the lead is a member of another group leave that group first + Some(_) => { + self.remove_from_group(leader, groups, alignments, uids, entities, &mut notifier); + None + }, + None => None, + }; + + let group = group.unwrap_or_else(|| { + let new_group = self.create_group(leader); + // Unwrap should not fail since we just found these entities and they should + // still exist Note: if there is an issue replace with a warn + groups.insert(leader, new_group).unwrap(); + // Inform + notifier(leader, ChangeNotification::NewLeader(leader)); + new_group + }); + + let member_plus_pets = with_pets(new_member, new_member_uid, alignments, entities); + + // Inform + members(group, &*groups, entities).for_each(|a| { + member_plus_pets.iter().for_each(|b| { + notifier(a, ChangeNotification::Added(*b)); + notifier(*b, ChangeNotification::Added(a)); + }) + }); + // Note: pets not informed + notifier(new_member, ChangeNotification::NewLeader(leader)); + + // Add group id for new member and pets + // Unwrap should not fail since we just found these entities and they should + // still exist + // Note: if there is an issue replace with a warn + member_plus_pets.iter().for_each(|e| { + let _ = groups.insert(*e, group).unwrap(); + }); + } + + pub fn new_pet( + &mut self, + pet: specs::Entity, + owner: specs::Entity, + groups: &mut GroupsMut, + entities: &specs::Entities, + notifier: &mut impl FnMut(specs::Entity, ChangeNotification), + ) { + let group = match groups.get(owner).copied() { + Some(group) => group, + None => { + let new_group = self.create_group(owner); + groups.insert(owner, new_group).unwrap(); + // Inform + notifier(owner, ChangeNotification::NewLeader(owner)); + new_group + }, + }; + + // Inform + members(group, &*groups, entities).for_each(|a| { + notifier(a, ChangeNotification::Added(pet)); + notifier(pet, ChangeNotification::Added(a)); + }); + + // Add + groups.insert(pet, group).unwrap(); + + if let Some(info) = self.group_info(group) { + notifier(pet, ChangeNotification::NewLeader(info.leader)); + } + } + + // Remove someone from a group if they are in one + // Don't need to check if they are in a group before calling this + // Also removes pets (ie call this if the pet no longer exists) + pub fn remove_from_group( + &mut self, + member: specs::Entity, + groups: &mut GroupsMut, + alignments: &Alignments, + uids: &Uids, + entities: &specs::Entities, + notifier: &mut impl FnMut(specs::Entity, ChangeNotification), + ) { + let group = match groups.get(member) { + Some(group) => *group, + None => return, + }; + + // If leaving entity was the leader disband the group + if self + .group_info(group) + .map(|info| info.leader == member) + .unwrap_or(false) + { + // Remove group + self.remove_group(group); + + (entities, uids, &*groups, alignments.maybe()) + .join() + .filter(|(_, _, g, _)| **g == group) + .fold( + HashMap::, Vec)>::new(), + |mut acc, (e, uid, _, alignment)| { + if let Some(Alignment::Owned(owner)) = alignment { + // Assumes owner will be in the group + acc.entry(*owner).or_default().1.push(e); + } else { + acc.entry(*uid).or_default().0 = Some(e); + } + + acc + }, + ) + .into_iter() + .map(|(_, v)| v) + .for_each(|(owner, pets)| { + if let Some(owner) = owner { + if !pets.is_empty() { + let mut members = pets.clone(); + members.push(owner); + + // New group + let new_group = self.create_group(owner); + for &member in &members { + groups.insert(member, new_group).unwrap(); + } + + let notification = ChangeNotification::NewGroup { + leader: owner, + members, + }; + + // TODO: don't clone + notifier(owner, notification.clone()); + pets.into_iter() + .for_each(|pet| notifier(pet, notification.clone())); + } else { + // If no pets just remove group + groups.remove(owner); + notifier(owner, ChangeNotification::NoGroup) + } + } else { + warn!( + "Something went wrong! The pet owner is missing from a group that the \ + pet is in" + ); + pets.into_iter() + .for_each(|pet| notifier(pet, ChangeNotification::NoGroup)); + } + }); + } else { + // Not leader + let leaving_member_uid = if let Some(uid) = uids.get(member) { + *uid + } else { + error!("Failed to retrieve uid for the new group member"); + return; + }; + + let leaving = with_pets(member, leaving_member_uid, alignments, entities); + + // If pets form new group + if leaving.len() > 1 { + let new_group = self.create_group(member); + + leaving.iter().for_each(|e| { + let _ = groups.insert(*e, new_group).unwrap(); + }); + + let notification = ChangeNotification::NewGroup { + leader: member, + members: leaving.clone(), + }; + + leaving + .iter() + .for_each(|e| notifier(*e, notification.clone())); + } else { + groups.remove(member); + notifier(member, ChangeNotification::NoGroup) + } + + // Inform remaining members + let mut num_members = 0; + members(group, &*groups, entities).for_each(|a| { + num_members += 1; + leaving.iter().for_each(|b| { + notifier(a, ChangeNotification::Removed(*b)); + }) + }); + + // If leader is the last one left then disband the group + // Assumes last member is the leader + if num_members == 1 { + if let Some(info) = self.group_info(group) { + let leader = info.leader; + self.remove_group(group); + groups.remove(leader); + notifier(leader, ChangeNotification::NoGroup); + } + } else if num_members == 0 { + error!("Somehow group has no members") + } + } + } + + // Assign new group leader + // Does nothing if new leader is not part of a group + pub fn assign_leader( + &mut self, + new_leader: specs::Entity, + groups: &Groups, + entities: &specs::Entities, + mut notifier: impl FnMut(specs::Entity, ChangeNotification), + ) { + let group = match groups.get(new_leader) { + Some(group) => *group, + None => return, + }; + + // Set new leader + self.groups[group.0 as usize].leader = new_leader; + + // Point to new leader + members(group, groups, entities).for_each(|e| { + notifier(e, ChangeNotification::NewLeader(e)); + }); + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index b99b575450..041a789b37 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -7,6 +7,7 @@ mod chat; mod controller; mod damage; mod energy; +pub mod group; mod inputs; mod inventory; mod last; @@ -28,13 +29,17 @@ pub use body::{ humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData, }; pub use character_state::{Attacking, CharacterState, StateUpdate}; -pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group, SpeechBubble, SpeechBubbleType}; +// TODO: replace chat::Group functionality with group::Group +pub use chat::{ + ChatMode, ChatMsg, ChatType, Faction, Group as ChatGroup, SpeechBubble, SpeechBubbleType, +}; pub use controller::{ - Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip, - MountState, Mounting, + Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input, + InventoryManip, MountState, Mounting, }; pub use damage::{Damage, DamageSource}; pub use energy::{Energy, EnergySource}; +pub use group::Group; pub use inputs::CanBuild; pub use inventory::{ item, diff --git a/common/src/event.rs b/common/src/event.rs index 7bcb6683db..1e2ba17543 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -35,6 +35,7 @@ pub enum ServerEvent { cause: comp::HealthSource, }, InventoryManip(EcsEntity, comp::InventoryManip), + GroupManip(EcsEntity, comp::GroupManip), Respawn(EcsEntity), Shoot { entity: EcsEntity, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index 1df92d9382..726d67321a 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -17,7 +17,7 @@ sum_type! { LightEmitter(comp::LightEmitter), Item(comp::Item), Scale(comp::Scale), - Alignment(comp::Alignment), + Group(comp::Group), MountState(comp::MountState), Mounting(comp::Mounting), Mass(comp::Mass), @@ -44,7 +44,7 @@ sum_type! { LightEmitter(PhantomData), Item(PhantomData), Scale(PhantomData), - Alignment(PhantomData), + Group(PhantomData), MountState(PhantomData), Mounting(PhantomData), Mass(PhantomData), @@ -71,7 +71,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::Alignment(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world), @@ -96,7 +96,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::Alignment(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world), @@ -123,7 +123,7 @@ impl sync::CompPacket for EcsCompPacket { }, EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::Alignment(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Group(_) => sync::handle_remove::(entity, world), EcsCompPhantom::MountState(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Mounting(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Mass(_) => sync::handle_remove::(entity, world), diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index a83dc6216e..4c3fda2aab 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -69,6 +69,8 @@ pub enum ServerMsg { /// An error occured while creating or deleting a character CharacterActionError(String), PlayerListUpdate(PlayerListUpdate), + GroupUpdate(comp::group::ChangeNotification), + GroupInvite(sync::Uid), StateAnswer(Result), /// Trigger cleanup for when the client goes back to the `Registered` state /// from an ingame state diff --git a/common/src/state.rs b/common/src/state.rs index f232d07163..c4aca3a85d 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -123,7 +123,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); // Register components send from clients -> server ecs.register::(); @@ -146,6 +146,7 @@ impl State { ecs.register::>(); ecs.register::>(); ecs.register::>(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); @@ -156,7 +157,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); ecs.register::(); // Register synced resources used by the ECS. @@ -168,9 +169,10 @@ impl State { ecs.insert(TerrainGrid::new().unwrap()); ecs.insert(BlockChange::default()); ecs.insert(TerrainChanges::default()); + ecs.insert(EventBus::::default()); // TODO: only register on the server ecs.insert(EventBus::::default()); - ecs.insert(EventBus::::default()); + ecs.insert(comp::group::GroupManager::default()); ecs.insert(RegionMap::new()); ecs diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 36b8afd4e9..4e0690ccac 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -2,6 +2,7 @@ use crate::{ comp::{ self, agent::Activity, + group, item::{tool::ToolKind, ItemKind}, Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel, @@ -29,6 +30,7 @@ impl<'a> System<'a> for Sys { Read<'a, UidAllocator>, Read<'a, Time>, Read<'a, DeltaTime>, + Read<'a, group::GroupManager>, Write<'a, EventBus>, Entities<'a>, ReadStorage<'a, Pos>, @@ -40,6 +42,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, CharacterState>, ReadStorage<'a, PhysicsState>, ReadStorage<'a, Uid>, + ReadStorage<'a, group::Group>, ReadExpect<'a, TerrainGrid>, ReadStorage<'a, Alignment>, ReadStorage<'a, Body>, @@ -55,6 +58,7 @@ impl<'a> System<'a> for Sys { uid_allocator, time, dt, + group_manager, event_bus, entities, positions, @@ -66,6 +70,7 @@ impl<'a> System<'a> for Sys { character_states, physics_states, uids, + groups, terrain, alignments, bodies, @@ -88,6 +93,7 @@ impl<'a> System<'a> for Sys { agent, controller, mount_state, + group, ) in ( &entities, &positions, @@ -102,9 +108,19 @@ impl<'a> System<'a> for Sys { &mut agents, &mut controllers, mount_states.maybe(), + groups.maybe(), ) .join() { + // Hack, replace with better system when groups are more sophisticated + // Override alignment if in a group + let alignment = group + .and_then(|g| group_manager.group_info(*g)) + .and_then(|info| uids.get(info.leader)) + .copied() + .map(Alignment::Owned) + .or(alignment.copied()); + // Skip mounted entities if mount_state .map(|ms| *ms != MountState::Unmounted) @@ -117,7 +133,7 @@ impl<'a> System<'a> for Sys { let mut inputs = &mut controller.inputs; - // Default to looking in orientation direction + // Default to looking in orientation direction (can be overriden below) inputs.look_dir = ori.0; const AVG_FOLLOW_DIST: f32 = 6.0; @@ -148,11 +164,9 @@ impl<'a> System<'a> for Sys { thread_rng().gen::() - 0.5, ) * 0.1 - *bearing * 0.003 - - if let Some(patrol_origin) = agent.patrol_origin { - Vec2::::from(pos.0 - patrol_origin) * 0.0002 - } else { - Vec2::zero() - }; + - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { + (pos.0 - patrol_origin).xy() * 0.0002 + }); // Stop if we're too close to a wall *bearing *= 0.1 @@ -169,8 +183,7 @@ impl<'a> System<'a> for Sys { .until(|block| block.is_solid()) .cast() .1 - .map(|b| b.is_none()) - .unwrap_or(true) + .map_or(true, |b| b.is_none()) { 0.9 } else { @@ -269,8 +282,7 @@ impl<'a> System<'a> for Sys { // Don't attack entities we are passive towards // TODO: This is here, it's a bit of a hack if let Some(alignment) = alignment { - if (*alignment).passive_towards(tgt_alignment) || tgt_stats.is_dead - { + if alignment.passive_towards(tgt_alignment) || tgt_stats.is_dead { do_idle = true; break 'activity; } @@ -437,7 +449,7 @@ impl<'a> System<'a> for Sys { } // Follow owner if we're too far, or if they're under attack - if let Some(Alignment::Owned(owner)) = alignment.copied() { + if let Some(Alignment::Owned(owner)) = alignment { (|| { let owner = uid_allocator.retrieve_entity_internal(owner.id())?; diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index d585171b0d..e8d83e9847 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - Alignment, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, - HealthSource, Loadout, Ori, Pos, Scale, Stats, + group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, + Loadout, Ori, Pos, Scale, Stats, }, event::{EventBus, LocalEvent, ServerEvent}, sync::Uid, @@ -26,10 +26,10 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Pos>, ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, - ReadStorage<'a, Alignment>, ReadStorage<'a, Body>, ReadStorage<'a, Stats>, ReadStorage<'a, Loadout>, + ReadStorage<'a, group::Group>, WriteStorage<'a, Attacking>, WriteStorage<'a, CharacterState>, ); @@ -44,10 +44,10 @@ impl<'a> System<'a> for Sys { positions, orientations, scales, - alignments, bodies, stats, loadouts, + groups, mut attacking_storage, character_states, ): Self::SystemData, @@ -71,23 +71,12 @@ impl<'a> System<'a> for Sys { attack.applied = true; // Go through all other entities - for ( - b, - uid_b, - pos_b, - ori_b, - scale_b_maybe, - alignment_b_maybe, - character_b, - stats_b, - body_b, - ) in ( + for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in ( &entities, &uids, &positions, &orientations, scales.maybe(), - alignments.maybe(), character_states.maybe(), &stats, &bodies, @@ -111,6 +100,17 @@ impl<'a> System<'a> for Sys { && pos.0.distance_squared(pos_b.0) < (rad_b + scale * attack.range).powi(2) && ori2.angle_between(pos_b2 - pos2) < attack.max_angle + (rad_b / pos2.distance(pos_b2)).atan() { + // See if entities are in the same group + let same_group = groups + .get(entity) + .map(|group_a| Some(group_a) == groups.get(b)) + .unwrap_or(false); + // Don't heal if outside group + // Don't damage in the same group + if same_group != (attack.base_healthchange > 0) { + continue; + } + // Weapon gives base damage let source = if attack.base_healthchange > 0 { DamageSource::Healing @@ -121,28 +121,6 @@ impl<'a> System<'a> for Sys { healthchange: attack.base_healthchange as f32, source, }; - let mut knockback = attack.knockback; - - // TODO: remove this, either it will remain unused or be used as a temporary - // gameplay balance - //// NPCs do less damage - //if agent_maybe.is_some() { - // healthchange = (healthchange / 1.5).min(-1.0); - //} - - // TODO: remove this when there is a better way to deal with alignment - // Don't heal NPCs - if (damage.healthchange > 0.0 && alignment_b_maybe - .map(|a| !a.is_friendly_to_players()) - .unwrap_or(true)) - // Don't hurt pets - || (damage.healthchange < 0.0 && alignment_b_maybe - .map(|b| Alignment::Owned(*uid).passive_towards(*b)) - .unwrap_or(false)) - { - damage.healthchange = 0.0; - knockback = 0.0; - } let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; @@ -160,10 +138,10 @@ impl<'a> System<'a> for Sys { }, }); } - if knockback != 0.0 { + if attack.knockback != 0.0 { local_emitter.emit(LocalEvent::ApplyForce { entity: b, - force: knockback + force: attack.knockback * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5), }); } diff --git a/common/src/sys/controller.rs b/common/src/sys/controller.rs index b615d6cdb1..055f7e8dd4 100644 --- a/common/src/sys/controller.rs +++ b/common/src/sys/controller.rs @@ -96,6 +96,9 @@ impl<'a> System<'a> for Sys { } server_emitter.emit(ServerEvent::InventoryManip(entity, manip)) }, + ControlEvent::GroupManip(manip) => { + server_emitter.emit(ServerEvent::GroupManip(entity, manip)) + }, ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)), } } diff --git a/server/src/client.rs b/server/src/client.rs index 95e6d96b91..5c14688ae2 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -18,6 +18,7 @@ pub struct Client { pub network_error: AtomicBool, pub last_ping: f64, pub login_msg_sent: bool, + pub invited_to_group: Option, } impl Component for Client { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index d65c62ab29..821857507c 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -2,7 +2,7 @@ //! To implement a new command, add an instance of `ChatCommand` to //! `CHAT_COMMANDS` and provide a handler function. -use crate::{Server, StateExt}; +use crate::{client::Client, Server, StateExt}; use chrono::{NaiveTime, Timelike}; use common::{ assets, @@ -557,6 +557,42 @@ fn handle_spawn( let new_entity = entity_base.build(); + // Add to group system if a pet + if matches!(alignment, comp::Alignment::Owned { .. }) { + let state = server.state(); + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + let mut group_manager = + state.ecs().write_resource::(); + group_manager.new_pet( + new_entity, + target, + &mut state.ecs().write_storage(), + &state.ecs().entities(), + &mut |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + } else if let Some(group) = match alignment { + comp::Alignment::Wild => None, + comp::Alignment::Enemy => Some(comp::group::ENEMY), + comp::Alignment::Npc | comp::Alignment::Tame => { + Some(comp::group::NPC) + }, + // TODO: handle + comp::Alignment::Owned(_) => unreachable!(), + } { + let _ = + server.state.ecs().write_storage().insert(new_entity, group); + } + if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) { server.notify_client( client, @@ -1161,7 +1197,7 @@ fn handle_group( return; } let ecs = server.state.ecs(); - if let Some(comp::Group(group)) = ecs.read_storage().get(client) { + if let Some(comp::ChatGroup(group)) = ecs.read_storage().get(client) { let mode = comp::ChatMode::Group(group.to_string()); let _ = ecs.write_storage().insert(client, mode.clone()); if !msg.is_empty() { @@ -1352,7 +1388,7 @@ fn handle_join_group( .state .ecs() .write_storage() - .insert(client, comp::Group(group.clone())) + .insert(client, comp::ChatGroup(group.clone())) .ok() .flatten() .map(|f| f.0); @@ -1369,7 +1405,7 @@ fn handle_join_group( .ecs() .write_storage() .remove(client) - .map(|comp::Group(f)| f) + .map(|comp::ChatGroup(f)| f) }; if let Some(group) = group_leave { server.state.send_chat( diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index fb8606e37a..b2924d9d15 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -1,7 +1,7 @@ use crate::{sys, Server, StateExt}; use common::{ comp::{ - self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, + self, group, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, util::Dir, @@ -36,12 +36,26 @@ pub fn handle_create_npc( scale: Scale, drop_item: Option, ) { + let group = match alignment { + Alignment::Wild => None, + Alignment::Enemy => Some(group::ENEMY), + Alignment::Npc | Alignment::Tame => Some(group::NPC), + // TODO: handle + Alignment::Owned(_) => None, + }; + let entity = server .state .create_npc(pos, stats, loadout, body) .with(scale) .with(alignment); + let entity = if let Some(group) = group { + entity.with(group) + } else { + entity + }; + let entity = if let Some(agent) = agent.into() { entity.with(agent) } else { diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs new file mode 100644 index 0000000000..6c4d4a232c --- /dev/null +++ b/server/src/events/group_manip.rs @@ -0,0 +1,304 @@ +use crate::{client::Client, Server}; +use common::{ + comp::{ + self, + group::{self, GroupManager}, + ChatType, GroupManip, + }, + msg::ServerMsg, + sync, + sync::WorldSyncExt, +}; +use specs::world::WorldExt; + +// TODO: turn chat messages into enums +pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { + let state = server.state_mut(); + + match manip { + GroupManip::Invite(uid) => { + let mut clients = state.ecs().write_storage::(); + let invitee = match state.ecs().entity_from_uid(uid.into()) { + Some(t) => t, + None => { + // Inform of failure + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Leadership transfer failed, target does not exist".to_owned(), + )); + } + return; + }, + }; + + let uids = state.ecs().read_storage::(); + let alignments = state.ecs().read_storage::(); + let agents = state.ecs().read_storage::(); + let mut already_has_invite = false; + let mut add_to_group = false; + // If client comp + if let (Some(client), Some(inviter_uid)) = (clients.get_mut(invitee), uids.get(entity)) + { + if client.invited_to_group.is_some() { + already_has_invite = true; + } else { + client.notify(ServerMsg::GroupInvite((*inviter_uid).into())); + client.invited_to_group = Some(entity); + } + // Would be cool to do this in agent system (e.g. add an invited + // component to replace the field on Client) + // TODO: move invites to component and make them time out + } else if matches!( + (alignments.get(invitee), agents.get(invitee)), + (Some(comp::Alignment::Npc), Some(_)) + ) { + add_to_group = true; + // Wipe agent state + let _ = state + .ecs() + .write_storage() + .insert(invitee, comp::Agent::default()); + } + + if already_has_invite { + // Inform inviter that there is already an invite + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Invite failed target already has a pending invite".to_owned(), + )); + } + } + + if add_to_group { + let mut group_manager = state.ecs().write_resource::(); + group_manager.add_group_member( + entity, + invitee, + &state.ecs().entities(), + &mut state.ecs().write_storage(), + &state.ecs().read_storage(), + &uids, + |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + } + }, + GroupManip::Accept => { + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + if let Some(inviter) = clients + .get_mut(entity) + .and_then(|c| c.invited_to_group.take()) + { + let mut group_manager = state.ecs().write_resource::(); + group_manager.add_group_member( + inviter, + entity, + &state.ecs().entities(), + &mut state.ecs().write_storage(), + &state.ecs().read_storage(), + &uids, + |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + } + }, + GroupManip::Reject => { + let mut clients = state.ecs().write_storage::(); + if let Some(inviter) = clients + .get_mut(entity) + .and_then(|c| c.invited_to_group.take()) + { + // Inform inviter of rejection + if let Some(client) = clients.get_mut(inviter) { + // TODO: say who rejected the invite + client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + } + } + }, + GroupManip::Leave => { + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + let mut group_manager = state.ecs().write_resource::(); + group_manager.remove_from_group( + entity, + &mut state.ecs().write_storage(), + &state.ecs().read_storage(), + &uids, + &state.ecs().entities(), + &mut |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + }, + GroupManip::Kick(uid) => { + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + + let target = match state.ecs().entity_from_uid(uid.into()) { + Some(t) => t, + None => { + // Inform of failure + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Leadership transfer failed, target does not exist".to_owned(), + )); + } + return; + }, + }; + let mut groups = state.ecs().write_storage::(); + let mut group_manager = state.ecs().write_resource::(); + // Make sure kicker is the group leader + match groups + .get(target) + .and_then(|group| group_manager.group_info(*group)) + { + Some(info) if info.leader == entity => { + // Remove target from group + group_manager.remove_from_group( + target, + &mut groups, + &state.ecs().read_storage(), + &uids, + &state.ecs().entities(), + &mut |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + + // Tell them the have been kicked + if let Some(client) = clients.get_mut(target) { + client.notify( + ChatType::Meta.server_msg("The group leader kicked you".to_owned()), + ); + } + // Tell kicker that they were succesful + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg("Kick complete".to_owned())); + } + }, + Some(_) => { + // Inform kicker that they are not the leader + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Kick failed: you are not the leader of the target's group".to_owned(), + )); + } + }, + None => { + // Inform kicker that the target is not in a group + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg( + "Kick failed: your target is not in a group".to_owned(), + ), + ); + } + }, + } + }, + GroupManip::AssignLeader(uid) => { + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + let target = match state.ecs().entity_from_uid(uid.into()) { + Some(t) => t, + None => { + // Inform of failure + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Leadership transfer failed, target does not exist".to_owned(), + )); + } + return; + }, + }; + let groups = state.ecs().read_storage::(); + let mut group_manager = state.ecs().write_resource::(); + // Make sure assigner is the group leader + match groups + .get(target) + .and_then(|group| group_manager.group_info(*group)) + { + Some(info) if info.leader == entity => { + // Assign target as group leader + group_manager.assign_leader( + target, + &groups, + &state.ecs().entities(), + |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + // Tell them they are the leader + if let Some(client) = clients.get_mut(target) { + client.notify(ChatType::Meta.server_msg( + "The group leader has passed leadership to you".to_owned(), + )); + } + // Tell the old leader that the transfer was succesful + if let Some(client) = clients.get_mut(target) { + client + .notify(ChatType::Meta.server_msg("Leadership transferred".to_owned())); + } + }, + Some(_) => { + // Inform transferer that they are not the leader + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg( + "Transfer failed: you are not the leader of the target's group" + .to_owned(), + ), + ); + } + }, + None => { + // Inform transferer that the target is not in a group + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Transfer failed: your target is not in a group".to_owned(), + )); + } + }, + } + }, + } +} diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 119ad8429b..f265c36bea 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -1,10 +1,11 @@ -use crate::{Server, StateExt}; +use crate::{client::Client, Server, StateExt}; use common::{ comp::{ self, item, slot::{self, Slot}, Pos, MAX_PICKUP_RANGE_SQR, }, + msg::ServerMsg, recipe::default_recipe_book, sync::{Uid, WorldSyncExt}, terrain::block::Block, @@ -222,6 +223,33 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv .ecs() .write_storage() .insert(tameable_entity, comp::Alignment::Owned(uid)); + + // Add to group system + let mut clients = state.ecs().write_storage::(); + let uids = state.ecs().read_storage::(); + let mut group_manager = state + .ecs() + .write_resource::( + ); + group_manager.new_pet( + tameable_entity, + entity, + &mut state.ecs().write_storage(), + &state.ecs().entities(), + &mut |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| { + c.notify(ServerMsg::GroupUpdate(g)) + }); + }, + ); + let _ = state .ecs() .write_storage() diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index fb26dad411..55783d5196 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -8,6 +8,7 @@ use entity_manipulation::{ handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up, handle_respawn, }; +use group_manip::handle_group; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; use inventory_manip::handle_inventory; use player::{handle_client_disconnect, handle_exit_ingame}; @@ -15,6 +16,7 @@ use specs::{Entity as EcsEntity, WorldExt}; mod entity_creation; mod entity_manipulation; +mod group_manip; mod interaction; mod inventory_manip; mod player; @@ -62,6 +64,7 @@ impl Server { ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change), ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), + ServerEvent::GroupManip(entity, manip) => handle_group(self, entity, manip), ServerEvent::Respawn(entity) => handle_respawn(&self, entity), ServerEvent::LandOnGround { entity, vel } => { handle_land_on_ground(&self, entity, vel) diff --git a/server/src/events/player.rs b/server/src/events/player.rs index 0e7bd7120d..fa802968b5 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -4,7 +4,7 @@ use crate::{ }; use common::{ comp, - comp::Player, + comp::{group, Player}, msg::{ClientState, PlayerListUpdate, ServerMsg}, sync::{Uid, UidAllocator}, }; @@ -22,6 +22,11 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { let maybe_client = state.ecs().write_storage::().remove(entity); let maybe_uid = state.read_component_cloned::(entity); let maybe_player = state.ecs().write_storage::().remove(entity); + let maybe_group = state + .ecs() + .write_storage::() + .get(entity) + .cloned(); if let (Some(mut client), Some(uid), Some(player)) = (maybe_client, maybe_uid, maybe_player) { // Tell client its request was successful client.allow_state(ClientState::Registered); @@ -29,13 +34,37 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { client.notify(ServerMsg::ExitIngameCleanup); let entity_builder = state.ecs_mut().create_entity().with(client).with(player); + + let entity_builder = match maybe_group { + Some(group) => entity_builder.with(group), + None => entity_builder, + }; + // Ensure UidAllocator maps this uid to the new entity let uid = entity_builder .world .write_resource::() .allocate(entity_builder.entity, Some(uid.into())); - entity_builder.with(uid).build(); + let new_entity = entity_builder.with(uid).build(); + if let Some(group) = maybe_group { + let mut group_manager = state.ecs().write_resource::(); + if group_manager + .group_info(group) + .map(|info| info.leader == entity) + .unwrap_or(false) + { + group_manager.assign_leader( + new_entity, + &state.ecs().read_storage(), + &state.ecs().entities(), + // Nothing actually changing + |_, _| {}, + ); + } + } } + // Erase group component to avoid group restructure when deleting the entity + state.ecs().write_storage::().remove(entity); // Delete old entity if let Err(e) = state.delete_entity_recorded(entity) { error!( diff --git a/server/src/lib.rs b/server/src/lib.rs index 2f5fff5ae9..9a43cb82db 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -656,6 +656,7 @@ impl Server { network_error: std::sync::atomic::AtomicBool::new(false), last_ping: self.state.get_time(), login_msg_sent: false, + invited_to_group: None, }; if self.settings().max_players diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 645e7843d4..24333d9bb3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -320,7 +320,7 @@ impl StateExt for State { comp::ChatType::GroupMeta(s) | comp::ChatType::Group(_, s) => { for (client, group) in ( &mut ecs.write_storage::(), - &ecs.read_storage::(), + &ecs.read_storage::(), ) .join() { @@ -346,6 +346,30 @@ impl StateExt for State { &mut self, entity: EcsEntity, ) -> Result<(), specs::error::WrongGeneration> { + // Remove entity from a group if they are in one + { + let mut clients = self.ecs().write_storage::(); + let uids = self.ecs().read_storage::(); + let mut group_manager = self.ecs().write_resource::(); + group_manager.remove_from_group( + entity, + &mut self.ecs().write_storage(), + &self.ecs().read_storage(), + &uids, + &self.ecs().entities(), + &mut |entity, group_change| { + clients + .get_mut(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + }, + ); + } + let (maybe_uid, maybe_pos) = ( self.ecs().read_storage::().get(entity).copied(), self.ecs().read_storage::().get(entity).copied(), diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 55da254f51..adcc76bd30 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,7 +1,7 @@ use super::SysTimer; use common::{ comp::{ - Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, + Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, }, msg::EcsCompPacket, @@ -48,7 +48,7 @@ pub struct TrackedComps<'a> { pub scale: ReadStorage<'a, Scale>, pub mounting: ReadStorage<'a, Mounting>, pub mount_state: ReadStorage<'a, MountState>, - pub alignment: ReadStorage<'a, Alignment>, + pub group: ReadStorage<'a, Group>, pub mass: ReadStorage<'a, Mass>, pub collider: ReadStorage<'a, Collider>, pub sticky: ReadStorage<'a, Sticky>, @@ -105,7 +105,7 @@ impl<'a> TrackedComps<'a> { .get(entity) .cloned() .map(|c| comps.push(c.into())); - self.alignment + self.group .get(entity) .cloned() .map(|c| comps.push(c.into())); @@ -151,7 +151,7 @@ pub struct ReadTrackers<'a> { pub scale: ReadExpect<'a, UpdateTracker>, pub mounting: ReadExpect<'a, UpdateTracker>, pub mount_state: ReadExpect<'a, UpdateTracker>, - pub alignment: ReadExpect<'a, UpdateTracker>, + pub group: ReadExpect<'a, UpdateTracker>, pub mass: ReadExpect<'a, UpdateTracker>, pub collider: ReadExpect<'a, UpdateTracker>, pub sticky: ReadExpect<'a, UpdateTracker>, @@ -184,7 +184,7 @@ impl<'a> ReadTrackers<'a> { .with_component(&comps.uid, &*self.scale, &comps.scale, filter) .with_component(&comps.uid, &*self.mounting, &comps.mounting, filter) .with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter) - .with_component(&comps.uid, &*self.alignment, &comps.alignment, filter) + .with_component(&comps.uid, &*self.group, &comps.group, filter) .with_component(&comps.uid, &*self.mass, &comps.mass, filter) .with_component(&comps.uid, &*self.collider, &comps.collider, filter) .with_component(&comps.uid, &*self.sticky, &comps.sticky, filter) @@ -214,7 +214,7 @@ pub struct WriteTrackers<'a> { scale: WriteExpect<'a, UpdateTracker>, mounting: WriteExpect<'a, UpdateTracker>, mount_state: WriteExpect<'a, UpdateTracker>, - alignment: WriteExpect<'a, UpdateTracker>, + group: WriteExpect<'a, UpdateTracker>, mass: WriteExpect<'a, UpdateTracker>, collider: WriteExpect<'a, UpdateTracker>, sticky: WriteExpect<'a, UpdateTracker>, @@ -236,7 +236,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.scale.record_changes(&comps.scale); trackers.mounting.record_changes(&comps.mounting); trackers.mount_state.record_changes(&comps.mount_state); - trackers.alignment.record_changes(&comps.alignment); + trackers.group.record_changes(&comps.group); trackers.mass.record_changes(&comps.mass); trackers.collider.record_changes(&comps.collider); trackers.sticky.record_changes(&comps.sticky); @@ -291,7 +291,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); - world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); From 6aba81051776a0c17ff09088be9a5125d3db1a89 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 26 Apr 2020 23:40:56 -0400 Subject: [PATCH 06/71] Add key to select entities --- assets/voxygen/i18n/en.ron | 3 ++- client/src/lib.rs | 1 - voxygen/src/hud/mod.rs | 7 +++++-- voxygen/src/session.rs | 24 ++++++++++++++++++++---- voxygen/src/settings.rs | 2 ++ voxygen/src/window.rs | 2 ++ 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 0d4c6c2f9d..8052ecf4b7 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -377,7 +377,8 @@ magically infused items?"#, "gameinput.freelook": "Free Look", "gameinput.autowalk": "Auto Walk", "gameinput.dance": "Dance", - + "gameinput.select": "Select Entity", + /// End GameInput section diff --git a/client/src/lib.rs b/client/src/lib.rs index 298c9bef15..08b10a84e6 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -996,7 +996,6 @@ impl Client { // the view distance match change_notification { Added(uid) => { - warn!("message to add: {}", uid); if !self.group_members.insert(uid) { warn!( "Received msg to add uid {} to the group members but they \ diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 39edeff12e..cd23a0223f 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -243,6 +243,7 @@ pub struct HudInfo { pub is_aiming: bool, pub is_first_person: bool, pub target_entity: Option, + pub selected_entity: Option, } pub enum Event { @@ -1047,8 +1048,10 @@ impl Hud { ) .join() .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead - && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity)) - ) + && (stats.health.current() != stats.health.maximum() + || info.target_entity.map_or(false, |e| e == *entity) + || info.selected_entity.map_or(false, |e| e == *entity) + )) // Don't show outside a certain range .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { pos.0.distance_squared(player_pos) diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 578dc9b0fa..a6f5e608b1 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -52,6 +52,8 @@ pub struct SessionState { free_look: bool, auto_walk: bool, is_aiming: bool, + target_entity: Option, + selected_entity: Option, } /// Represents an active game session (i.e., the one being played). @@ -86,6 +88,8 @@ impl SessionState { free_look: false, auto_walk: false, is_aiming: false, + target_entity: None, + selected_entity: None, } } @@ -234,7 +238,7 @@ impl PlayState for SessionState { let (build_pos, select_pos, target_entity) = under_cursor(&self.client.borrow(), cam_pos, cam_dir); // Throw out distance info, it will be useful in the future - let target_entity = target_entity.map(|x| x.0); + self.target_entity = target_entity.map(|x| x.0); let can_build = self .client @@ -527,6 +531,11 @@ impl PlayState for SessionState { let camera = self.scene.camera_mut(); camera.next_mode(self.client.borrow().is_admin()); }, + Event::InputUpdate(GameInput::Select, state) => { + if !state { + self.selected_entity = self.target_entity; + } + }, Event::AnalogGameInput(input) => match input { AnalogGameInput::MovementX(v) => { self.key_state.analog_matrix.x = v; @@ -676,7 +685,8 @@ impl PlayState for SessionState { self.scene.camera().get_mode(), camera::CameraMode::FirstPerson ), - target_entity, + target_entity: self.target_entity, + selected_entity: self.selected_entity, }, ); @@ -948,7 +958,7 @@ impl PlayState for SessionState { let scene_data = SceneData { state: client.state(), player_entity: client.entity(), - target_entity, + target_entity: self.target_entity, loaded_distance: client.loaded_distance(), view_distance: client.view_distance().unwrap_or(1), tick: client.get_tick(), @@ -1003,6 +1013,7 @@ impl PlayState for SessionState { let scene_data = SceneData { state: client.state(), player_entity: client.entity(), + target_entity: self.target_entity, loaded_distance: client.loaded_distance(), view_distance: client.view_distance().unwrap_or(1), tick: client.get_tick(), @@ -1097,14 +1108,18 @@ fn under_cursor( let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; // Move position up from the feet let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); + // Distance squared from camera to the entity let dist_sqr = pos.distance_squared(cam_pos); (e, pos, radius, dist_sqr) }) + // Roughly filter out entities farther than ray distance + .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2)) // Ignore entities intersecting the camera .filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2)) - .filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2)) + // Substract sphere radius from distance to the camera .map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r)) .collect::>(); + // Sort by distance nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap()); let seg_ray = LineSegment3 { @@ -1115,6 +1130,7 @@ fn under_cursor( let target_entity = nearby .iter() .map(|(e, p, r, _)| (e, *p, r)) + // Find first one that intersects the ray segment .find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2)) .and_then(|(e, p, r)| { let dist_to_player = p.distance(player_pos); diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 1767ee8764..d89833dd23 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -169,6 +169,7 @@ impl ControlSettings { GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9), GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q), GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt), + GameInput::Select => KeyMouse::Key(VirtualKeyCode::I), } } } @@ -234,6 +235,7 @@ impl Default for ControlSettings { GameInput::Slot9, GameInput::Slot10, GameInput::SwapLoadout, + GameInput::Select, ]; for game_input in game_inputs { new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index c634a332f5..d338d2ca10 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -67,6 +67,7 @@ pub enum GameInput { FreeLook, AutoWalk, CycleCamera, + Select, } impl GameInput { @@ -123,6 +124,7 @@ impl GameInput { GameInput::Slot9 => "gameinput.slot9", GameInput::Slot10 => "gameinput.slot10", GameInput::SwapLoadout => "gameinput.swaploadout", + GameInput::Select => "gameinput.select", } } From d9e3937a823bddadb2e32f86a6f5c9c3c1a99b65 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Sun, 12 Jul 2020 02:39:50 +0200 Subject: [PATCH 07/71] Basic UI Basic ui for groups and group window --- assets/voxygen/element/buttons/group.png | 3 + .../voxygen/element/buttons/group_hover.png | 3 + .../voxygen/element/buttons/group_press.png | 3 + assets/voxygen/i18n/en.ron | 10 +- client/src/lib.rs | 48 ++- common/src/comp/group.rs | 6 + common/src/state.rs | 4 +- server/src/cmd.rs | 22 +- server/src/events/entity_manipulation.rs | 2 +- server/src/events/group_manip.rs | 32 +- server/src/events/inventory_manip.rs | 14 +- server/src/events/player.rs | 2 +- server/src/state_ext.rs | 4 +- voxygen/src/hud/buttons.rs | 24 +- voxygen/src/hud/group.rs | 329 +++++++++++++++ voxygen/src/hud/img_ids.rs | 7 +- voxygen/src/hud/mod.rs | 54 ++- voxygen/src/hud/social.rs | 392 +++++++++++++++--- voxygen/src/session.rs | 23 +- 19 files changed, 869 insertions(+), 113 deletions(-) create mode 100644 assets/voxygen/element/buttons/group.png create mode 100644 assets/voxygen/element/buttons/group_hover.png create mode 100644 assets/voxygen/element/buttons/group_press.png create mode 100644 voxygen/src/hud/group.rs diff --git a/assets/voxygen/element/buttons/group.png b/assets/voxygen/element/buttons/group.png new file mode 100644 index 0000000000..e059af9154 --- /dev/null +++ b/assets/voxygen/element/buttons/group.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55e113c52f38efe8fc5dcea32099f8821cc1c6b72810244412d8d4398ae36cac +size 2152 diff --git a/assets/voxygen/element/buttons/group_hover.png b/assets/voxygen/element/buttons/group_hover.png new file mode 100644 index 0000000000..c405b87a66 --- /dev/null +++ b/assets/voxygen/element/buttons/group_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86380af53936c13c354d13616e662d9e25ca115d7857b883fda74a6cb7f57f74 +size 2243 diff --git a/assets/voxygen/element/buttons/group_press.png b/assets/voxygen/element/buttons/group_press.png new file mode 100644 index 0000000000..0fd69da245 --- /dev/null +++ b/assets/voxygen/element/buttons/group_press.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dda830676b251480e41ffe3cbd85a0531893bb299a429b0d26dce9edd5cdccc6 +size 2253 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 8052ecf4b7..a52a535254 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -65,6 +65,7 @@ VoxygenLocalization( "common.create": "Create", "common.okay": "Okay", "common.accept": "Accept", + "common.reject": "Reject", "common.disclaimer": "Disclaimer", "common.cancel": "Cancel", "common.none": "None", @@ -319,7 +320,14 @@ magically infused items?"#, "hud.crafting.craft": "Craft", "hud.crafting.tool_cata": "Requires:", - "hud.spell": "Spells", + "hud.group": "Group", + "hud.group.invite_to_join": "{name} invited you to their group.", + "hud.group.invite": "Invite", + "hud.group.kick": "Kick", + "hud.group.assign_leader": "Assign Leader", + "hud.group.leave": "Leave Group", + + "hud.spell": "Spells", "hud.free_look_indicator": "Free look active", "hud.auto_walk_indicator": "Auto walk active", diff --git a/client/src/lib.rs b/client/src/lib.rs index 08b10a84e6..4347b9af19 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -383,7 +383,7 @@ impl Client { } pub fn pick_up(&mut self, entity: EcsEntity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + if let Some(uid) = self.state.read_component_copied(entity) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::InventoryManip( InventoryManip::Pickup(uid), @@ -436,6 +436,12 @@ impl Client { pub fn group_leader(&self) -> Option { self.group_leader } + pub fn send_group_invite(&mut self, invitee: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Invite(invitee) ))) + .unwrap() + } + pub fn accept_group_invite(&mut self) { // Clear invite self.group_invite.take(); @@ -461,22 +467,18 @@ impl Client { ))).unwrap(); } - pub fn kick_from_group(&mut self, entity: specs::Entity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { - self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::Kick(uid), - ))).unwrap(); - } + pub fn kick_from_group(&mut self, uid: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Kick(uid), + ))).unwrap(); } - pub fn assign_group_leader(&mut self, entity: specs::Entity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { - self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::AssignLeader(uid), - ))).unwrap(); - } + pub fn assign_group_leader(&mut self, uid: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::AssignLeader(uid), + ))).unwrap(); } pub fn is_mounted(&self) -> bool { @@ -488,7 +490,7 @@ impl Client { } pub fn mount(&mut self, entity: EcsEntity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + if let Some(uid) = self.state.read_component_copied(entity) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::Mount(uid))) .unwrap(); @@ -1069,7 +1071,7 @@ impl Client { self.state.ecs_mut().apply_entity_package(entity_package); }, ServerMsg::DeleteEntity(entity) => { - if self.state.read_component_cloned::(self.entity) != Some(entity) { + if self.uid() != Some(entity) { self.state .ecs_mut() .delete_entity_and_clear_from_uid_allocator(entity.0); @@ -1179,6 +1181,11 @@ impl Client { /// Get the player's entity. pub fn entity(&self) -> EcsEntity { self.entity } + /// Get the player's Uid. + pub fn uid(&self) -> Option { + self.state.read_component_copied(self.entity) + } + /// Get the client state pub fn get_client_state(&self) -> ClientState { self.client_state } @@ -1230,7 +1237,7 @@ impl Client { pub fn is_admin(&self) -> bool { let client_uid = self .state - .read_component_cloned::(self.entity) + .read_component_copied::(self.entity) .expect("Client doesn't have a Uid!!!"); self.player_list @@ -1241,8 +1248,7 @@ impl Client { /// Clean client ECS state fn clean_state(&mut self) { let client_uid = self - .state - .read_component_cloned::(self.entity) + .uid() .map(|u| u.into()) .expect("Client doesn't have a Uid!!!"); @@ -1313,7 +1319,7 @@ impl Client { comp::ChatType::Tell(from, to) => { let from_alias = alias_of_uid(from); let to_alias = alias_of_uid(to); - if Some(from) == self.state.ecs().read_storage::().get(self.entity) { + if Some(*from) == self.uid() { format!("To [{}]: {}", to_alias, message) } else { format!("From [{}]: {}", from_alias, message) diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 339919a9dc..99f2167cc3 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -130,6 +130,12 @@ impl GroupManager { uids: &Uids, mut notifier: impl FnMut(specs::Entity, ChangeNotification), ) { + // Ensure leader is not inviting themselves + if leader == new_member { + warn!("Attempt to form group with leader as the only member (this is disallowed)"); + return; + } + // Get uid let new_member_uid = if let Some(uid) = uids.get(new_member) { *uid diff --git a/common/src/state.rs b/common/src/state.rs index c4aca3a85d..9d5a992e04 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -198,8 +198,8 @@ impl State { } /// Read a component attributed to a particular entity. - pub fn read_component_cloned(&self, entity: EcsEntity) -> Option { - self.ecs.read_storage().get(entity).cloned() + pub fn read_component_copied(&self, entity: EcsEntity) -> Option { + self.ecs.read_storage().get(entity).copied() } /// Get a read-only reference to the storage of a particular component type. diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 821857507c..344f7c66b0 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -227,7 +227,7 @@ fn handle_jump( action: &ChatCommand, ) { if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(current_pos) => { server .state @@ -252,7 +252,7 @@ fn handle_goto( if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { if server .state - .read_component_cloned::(target) + .read_component_copied::(target) .is_some() { server @@ -463,9 +463,9 @@ fn handle_tp( ); return; }; - if let Some(_pos) = server.state.read_component_cloned::(target) { + if let Some(_pos) = server.state.read_component_copied::(target) { if let Some(player) = opt_player { - if let Some(pos) = server.state.read_component_cloned::(player) { + if let Some(pos) = server.state.read_component_copied::(player) { server.state.write_component(target, pos); server.state.write_component(target, comp::ForceUpdate); } else { @@ -510,7 +510,7 @@ fn handle_spawn( (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => { let uid = server .state - .read_component_cloned(target) + .read_component_copied(target) .expect("Expected player to have a UID"); if let Some(alignment) = parse_alignment(uid, &opt_align) { let amount = opt_amount @@ -521,7 +521,7 @@ fn handle_spawn( let ai = opt_ai.unwrap_or_else(|| "true".to_string()); - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let agent = if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment { @@ -630,7 +630,7 @@ fn handle_spawn_training_dummy( _args: String, _action: &ChatCommand, ) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let vel = Vec3::new( rand::thread_rng().gen_range(-2.0, 3.0), @@ -997,7 +997,7 @@ fn handle_explosion( let ecs = server.state.ecs(); - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { ecs.read_resource::>() .emit_now(ServerEvent::Explosion { @@ -1020,7 +1020,7 @@ fn handle_waypoint( _args: String, _action: &ChatCommand, ) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let time = server.state.ecs().read_resource(); let _ = server @@ -1056,7 +1056,7 @@ fn handle_adminify( Some(player) => { let is_admin = if server .state - .read_component_cloned::(player) + .read_component_copied::(player) .is_some() { ecs.write_storage::().remove(player); @@ -1662,7 +1662,7 @@ fn handle_remove_lights( action: &ChatCommand, ) { let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32); - let opt_player_pos = server.state.read_component_cloned::(target); + let opt_player_pos = server.state.read_component_copied::(target); let mut to_delete = vec![]; match opt_player_pos { diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 259cd87859..8033ccebdb 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -189,7 +189,7 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { .is_some() { let respawn_point = state - .read_component_cloned::(entity) + .read_component_copied::(entity) .map(|wp| wp.get_pos()) .unwrap_or(state.ecs().read_resource::().0); diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index 6c4d4a232c..2c6447541b 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -10,6 +10,7 @@ use common::{ sync::WorldSyncExt, }; use specs::world::WorldExt; +use tracing::warn; // TODO: turn chat messages into enums pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { @@ -23,15 +24,26 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani None => { // Inform of failure if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg( - "Leadership transfer failed, target does not exist".to_owned(), - )); + client.notify( + ChatType::Meta + .server_msg("Invite failed, target does not exist".to_owned()), + ); } return; }, }; let uids = state.ecs().read_storage::(); + + // Check if entity is trying to invite themselves to a group + if uids + .get(entity) + .map_or(false, |inviter_uid| *inviter_uid == uid) + { + warn!("Entity tried to invite themselves into a group"); + return; + } + let alignments = state.ecs().read_storage::(); let agents = state.ecs().read_storage::(); let mut already_has_invite = false; @@ -54,10 +66,15 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ) { add_to_group = true; // Wipe agent state + drop(agents); let _ = state .ecs() .write_storage() .insert(invitee, comp::Agent::default()); + } else { + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + } } if already_has_invite { @@ -76,7 +93,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani invitee, &state.ecs().entities(), &mut state.ecs().write_storage(), - &state.ecs().read_storage(), + &alignments, &uids, |entity, group_change| { clients @@ -163,9 +180,10 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani None => { // Inform of failure if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg( - "Leadership transfer failed, target does not exist".to_owned(), - )); + client.notify( + ChatType::Meta + .server_msg("Kick failed, target does not exist".to_owned()), + ); } return; }, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index f265c36bea..e1d658fb04 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -167,10 +167,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv thrown_items.push(( *pos, state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), *kind, )); @@ -185,7 +185,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv state.read_storage::().get(entity) { let uid = state - .read_component_cloned(entity) + .read_component_copied(entity) .expect("Expected player to have a UID"); if ( &state.read_storage::(), @@ -339,7 +339,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv dropped_items.push(( *pos, state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), item, )); @@ -371,10 +371,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv for _ in 0..amount { dropped_items.push(( state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), item.clone(), )); @@ -405,7 +405,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv + Vec3::unit_z() * 15.0 + Vec3::::zero().map(|_| rand::thread_rng().gen::() - 0.5) * 4.0; - let uid = state.read_component_cloned::(entity); + let uid = state.read_component_copied::(entity); let mut new_entity = state .create_object(Default::default(), match kind { diff --git a/server/src/events/player.rs b/server/src/events/player.rs index fa802968b5..1e777c1daf 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -20,7 +20,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { // Note: If other `ServerEvent`s are referring to this entity they will be // disrupted let maybe_client = state.ecs().write_storage::().remove(entity); - let maybe_uid = state.read_component_cloned::(entity); + let maybe_uid = state.read_component_copied::(entity); let maybe_player = state.ecs().write_storage::().remove(entity); let maybe_group = state .ecs() diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 24333d9bb3..cdce5d44d3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -173,7 +173,7 @@ impl StateExt for State { self.write_component(entity, comp::CharacterState::default()); self.write_component( entity, - comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()), + comp::Alignment::Owned(self.read_component_copied(entity).unwrap()), ); // Set the character id for the player @@ -213,7 +213,7 @@ impl StateExt for State { // Notify clients of a player list update let client_uid = self - .read_component_cloned::(entity) + .read_component_copied::(entity) .map(|u| u) .expect("Client doesn't have a Uid!!!"); diff --git a/voxygen/src/hud/buttons.rs b/voxygen/src/hud/buttons.rs index 8ae1f6b7c4..499242ef1b 100644 --- a/voxygen/src/hud/buttons.rs +++ b/voxygen/src/hud/buttons.rs @@ -41,7 +41,7 @@ widget_ids! { crafting_button_bg, crafting_text, crafting_text_bg, - + group_button, } } #[derive(WidgetCommon)] @@ -98,6 +98,7 @@ pub enum Event { ToggleSocial, ToggleSpell, ToggleCrafting, + ToggleGroup, } impl<'a> Widget for Buttons<'a> { @@ -360,6 +361,7 @@ impl<'a> Widget for Buttons<'a> { .color(TEXT_COLOR) .set(state.ids.spellbook_text, ui); } + // Crafting if Button::image(self.imgs.crafting_icon) .w_h(25.0, 25.0) @@ -396,6 +398,26 @@ impl<'a> Widget for Buttons<'a> { .color(TEXT_COLOR) .set(state.ids.crafting_text, ui); } + + // Group + if Button::image(self.imgs.group_icon) + .w_h(49.0, 26.0) + .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .hover_image(self.imgs.group_icon_hover) + .press_image(self.imgs.group_icon_press) + .with_tooltip( + self.tooltip_manager, + &localized_strings.get("hud.group"), + "", + &button_tooltip, + ) + .bottom_offset(TOOLTIP_UPSHIFT) + .set(state.ids.group_button, ui) + .was_clicked() + { + return Some(Event::ToggleGroup); + } + None } } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs new file mode 100644 index 0000000000..24b43e360b --- /dev/null +++ b/voxygen/src/hud/group.rs @@ -0,0 +1,329 @@ +use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, TEXT_COLOR_GREY, UI_MAIN}; + +use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use client::{self, Client}; +use common::{ + comp::Stats, + sync::{Uid, WorldSyncExt}, +}; +use conrod_core::{ + color, + widget::{self, Button, Image, Rectangle, Scrollbar, Text}, + widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, +}; +use specs::WorldExt; +use std::time::Instant; + +widget_ids! { + pub struct Ids { + bg, + title, + close, + btn_bg, + btn_friend, + btn_leader, + btn_link, + btn_kick, + btn_leave, + members[], + invite_bubble, + bubble_frame, + btn_accept, + btn_decline, + // TEST + test_leader, + test_member1, + test_member2, + test_member3, + test_member4, + test_member5, + } +} + +pub struct State { + ids: Ids, + // Holds the time when selection is made since this selection can be overriden + // by selecting an entity in-game + selected_uid: Option<(Uid, Instant)>, + // Selected group member + selected_member: Option, +} + +#[derive(WidgetCommon)] +pub struct Group<'a> { + show: &'a Show, + client: &'a Client, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + + selected_entity: Option<(specs::Entity, Instant)>, + + #[conrod(common_builder)] + common: widget::CommonBuilder, +} + +impl<'a> Group<'a> { + pub fn new( + show: &'a Show, + client: &'a Client, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, + ) -> Self { + Self { + show, + client, + imgs, + fonts, + localized_strings, + selected_entity, + common: widget::CommonBuilder::default(), + } + } +} + +pub enum Event { + Close, + Accept, + Reject, + Kick(Uid), + LeaveGroup, + AssignLeader(Uid), +} + +impl<'a> Widget for Group<'a> { + type Event = Vec; + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + Self::State { + ids: Ids::new(id_gen), + selected_uid: None, + selected_member: None, + } + } + + #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { state, ui, .. } = args; + + let mut events = Vec::new(); + + let player_leader = true; + let in_group = true; + let open_invite = false; + + if in_group || open_invite { + // Frame + Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) + .bottom_left_with_margins_on(ui.window, 220.0, 10.0) + .set(state.ids.bg, ui); + if open_invite { + // yellow animated border + } + } + + // Buttons + if in_group { + Text::new("Group Name") + .mid_top_with_margin_on(state.ids.bg, 2.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Add to Friends") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Leave Group") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leave, ui) + .was_clicked() + {}; + // Group leader functions + if player_leader { + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Assign Leader") + .label_color(TEXT_COLOR) // Grey when no player is selected + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leader, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Link Group") + .label_color(TEXT_COLOR_GREY) // Change this when the linking is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_link, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) + .down_from(state.ids.btn_link, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Kick") + .label_color(TEXT_COLOR) // Grey when no player is selected + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_kick, ui) + .was_clicked() + {}; + } + // Group Members, only character names, cut long names when they exceed the button size + // TODO Insert loop here + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Leader") // Grey when no player is selected + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_leader, ui) + .was_clicked() + { + //Select the Leader + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_leader, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member1, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member1, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member2, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member2, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member3, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member3, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member4, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member4, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member5, ui) + .was_clicked() + { + // Select the group member + }; + // Maximum of 6 Players/Npcs per Group + // Player pets count as group members, too. They are not counted into the maximum group size. + + } + if open_invite { + //self.show.group = true; Auto open group menu + Text::new("Player wants to invite you!") + .mid_top_with_margin_on(state.ids.bg, 20.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("[U] Accept") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("[I] Decline") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + + } + events + } +} diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 240860dd6e..afb6c54b3f 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -74,6 +74,8 @@ image_ids! { crafting_icon_hover: "voxygen.element.buttons.anvil_hover", crafting_icon_press: "voxygen.element.buttons.anvil_press", + // Group Window + // Chat-Arrows chat_arrow: "voxygen.element.buttons.arrow_down", @@ -94,7 +96,6 @@ image_ids! { slider_indicator_small: "voxygen.element.slider.indicator_round", // Buttons - settings: "voxygen.element.buttons.settings", settings_hover: "voxygen.element.buttons.settings_hover", settings_press: "voxygen.element.buttons.settings_press", @@ -111,6 +112,10 @@ image_ids! { spellbook_hover: "voxygen.element.buttons.spellbook_hover", spellbook_press: "voxygen.element.buttons.spellbook_press", + group_icon: "voxygen.element.buttons.group", + group_icon_hover: "voxygen.element.buttons.group_hover", + group_icon_press: "voxygen.element.buttons.group_press", + // Skill Icons twohsword_m1: "voxygen.element.icons.2hsword_m1", twohsword_m2: "voxygen.element.icons.2hsword_m2", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index cd23a0223f..93caf26a22 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -3,6 +3,7 @@ mod buttons; mod chat; mod crafting; mod esc_menu; +mod group; mod hotbar; mod img_ids; mod item_imgs; @@ -30,6 +31,7 @@ use chat::Chat; use chrono::NaiveTime; use crafting::Crafting; use esc_menu::EscMenu; +use group::Group; use img_ids::Imgs; use item_imgs::ItemImgs; use map::Map; @@ -69,7 +71,7 @@ const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0); const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0); const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); -//const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5); +const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5); const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4); //const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1); @@ -208,6 +210,7 @@ widget_ids! { social_window, crafting_window, settings_window, + group_window, // Free look indicator free_look_txt, @@ -243,7 +246,7 @@ pub struct HudInfo { pub is_aiming: bool, pub is_first_person: bool, pub target_entity: Option, - pub selected_entity: Option, + pub selected_entity: Option<(specs::Entity, std::time::Instant)>, } pub enum Event { @@ -299,6 +302,12 @@ pub enum Event { ChangeAutoWalkBehavior(PressBehavior), ChangeStopAutoWalkOnInput(bool), CraftRecipe(String), + InviteMember(common::sync::Uid), + AcceptInvite, + RejectInvite, + KickMember(common::sync::Uid), + LeaveGroup, + AssignLeader(common::sync::Uid), } // TODO: Are these the possible layouts we want? @@ -354,6 +363,7 @@ pub struct Show { bag: bool, social: bool, spell: bool, + group: bool, esc_menu: bool, open_windows: Windows, map: bool, @@ -388,6 +398,11 @@ impl Show { } } + fn group(&mut self, open: bool) { + self.group = open; + self.want_grab = !open; + } + fn social(&mut self, open: bool) { if !self.esc_menu { self.social = open; @@ -416,6 +431,8 @@ impl Show { fn toggle_map(&mut self) { self.map(!self.map) } + fn toggle_group(&mut self) { self.group(!self.group) } + fn toggle_mini_map(&mut self) { self.mini_map = !self.mini_map; } fn settings(&mut self, open: bool) { @@ -600,6 +617,7 @@ impl Hud { ui: true, social: false, spell: false, + group: false, mini_map: true, settings_tab: SettingsTab::Interface, social_tab: SocialTab::Online, @@ -1050,7 +1068,7 @@ impl Hud { .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity) - || info.selected_entity.map_or(false, |e| e == *entity) + || info.selected_entity.map_or(false, |s| s.0 == *entity) )) // Don't show outside a certain range .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { @@ -1551,6 +1569,7 @@ impl Hud { Some(buttons::Event::ToggleSpell) => self.show.toggle_spell(), Some(buttons::Event::ToggleMap) => self.show.toggle_map(), Some(buttons::Event::ToggleCrafting) => self.show.toggle_crafting(), + Some(buttons::Event::ToggleGroup) => self.show.toggle_group(), None => {}, } } @@ -1875,6 +1894,7 @@ impl Hud { &self.imgs, &self.fonts, &self.voxygen_i18n, + info.selected_entity, ) .set(self.ids.social_window, ui_widgets) { @@ -1883,6 +1903,34 @@ impl Hud { social::Event::ChangeSocialTab(social_tab) => { self.show.open_social_tab(social_tab) }, + social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), + social::Event::Accept => events.push(Event::AcceptInvite), + social::Event::Reject => events.push(Event::RejectInvite), + social::Event::Kick(uid) => events.push(Event::KickMember(uid)), + social::Event::LeaveGroup => events.push(Event::LeaveGroup), + social::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), + } + } + } + // Group Window + if self.show.group { + for event in Group::new( + &self.show, + client, + &self.imgs, + &self.fonts, + &self.voxygen_i18n, + info.selected_entity, + ) + .set(self.ids.group_window, ui_widgets) + { + match event { + group::Event::Close => self.show.social(false), + group::Event::Accept => events.push(Event::AcceptInvite), + group::Event::Reject => events.push(Event::RejectInvite), + 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)), } } } diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 1bf7164bbe..cfbe8b9e7b 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -2,11 +2,17 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; +use common::{ + comp::Stats, + sync::{Uid, WorldSyncExt}, +}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; +use specs::WorldExt; +use std::time::Instant; widget_ids! { pub struct Ids { @@ -25,9 +31,27 @@ widget_ids! { friends_test, faction_test, player_names[], + group, + group_invite, + member_names[], + accept_invite_button, + reject_invite_button, + invite_button, + kick_button, + assign_leader_button, + leave_button, } } +pub struct State { + ids: Ids, + // Holds the time when selection is made since this selection can be overriden + // by selecting an entity in-game + selected_uid: Option<(Uid, Instant)>, + // Selected group member + selected_member: Option, +} + pub enum SocialTab { Online, Friends, @@ -42,6 +66,8 @@ pub struct Social<'a> { fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, + #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -53,6 +79,7 @@ impl<'a> Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { show, @@ -60,6 +87,7 @@ impl<'a> Social<'a> { imgs, fonts, localized_strings, + selected_entity, common: widget::CommonBuilder::default(), } } @@ -68,24 +96,32 @@ impl<'a> Social<'a> { pub enum Event { Close, ChangeSocialTab(SocialTab), + Invite(Uid), + Accept, + Reject, + Kick(Uid), + LeaveGroup, + AssignLeader(Uid), } impl<'a> Widget for Social<'a> { type Event = Vec; - type State = Ids; + type State = State; type Style = (); - fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Ids::new(id_gen) } + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + Self::State { + ids: Ids::new(id_gen), + selected_uid: None, + selected_member: None, + } + } #[allow(clippy::unused_unit)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } fn update(self, args: widget::UpdateArgs) -> Self::Event { - let widget::UpdateArgs { - /* id, */ state: ids, - ui, - .. - } = args; + let widget::UpdateArgs { state, ui, .. } = args; let mut events = Vec::new(); @@ -93,15 +129,15 @@ impl<'a> Widget for Social<'a> { .top_left_with_margins_on(ui.window, 200.0, 25.0) .color(Some(UI_MAIN)) .w_h(103.0 * 4.0, 122.0 * 4.0) - .set(ids.social_frame, ui); + .set(state.ids.social_frame, ui); // X-Button if Button::image(self.imgs.close_button) .w_h(28.0, 28.0) .hover_image(self.imgs.close_button_hover) .press_image(self.imgs.close_button_press) - .top_right_with_margins_on(ids.social_frame, 0.0, 0.0) - .set(ids.social_close, ui) + .top_right_with_margins_on(state.ids.social_frame, 0.0, 0.0) + .set(state.ids.social_close, ui) .was_clicked() { events.push(Event::Close); @@ -109,32 +145,32 @@ impl<'a> Widget for Social<'a> { // Title Text::new(&self.localized_strings.get("hud.social")) - .mid_top_with_margin_on(ids.social_frame, 6.0) + .mid_top_with_margin_on(state.ids.social_frame, 6.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) - .set(ids.social_title, ui); + .set(state.ids.social_title, ui); // Alignment Rectangle::fill_with([99.0 * 4.0, 112.0 * 4.0], color::TRANSPARENT) - .mid_top_with_margin_on(ids.social_frame, 8.0 * 4.0) - .set(ids.align, ui); + .mid_top_with_margin_on(state.ids.social_frame, 8.0 * 4.0) + .set(state.ids.align, ui); // Content Alignment Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT) - .middle_of(ids.frame) + .middle_of(state.ids.frame) .scroll_kids() .scroll_kids_vertically() - .set(ids.content_align, ui); - Scrollbar::y_axis(ids.content_align) + .set(state.ids.content_align, ui); + Scrollbar::y_axis(state.ids.content_align) .thickness(5.0) .rgba(0.33, 0.33, 0.33, 1.0) - .set(ids.scrollbar, ui); + .set(state.ids.scrollbar, ui); // Frame Image::new(self.imgs.social_frame) .w_h(99.0 * 4.0, 100.0 * 4.0) - .mid_bottom_of(ids.align) + .mid_bottom_of(state.ids.align) .color(Some(UI_MAIN)) - .set(ids.frame, ui); + .set(state.ids.frame, ui); // Online Tab @@ -154,14 +190,14 @@ impl<'a> Widget for Social<'a> { } else { self.imgs.social_button_press }) - .top_left_with_margins_on(ids.align, 4.0, 0.0) + .top_left_with_margins_on(state.ids.align, 4.0, 0.0) .label(&self.localized_strings.get("hud.social.online")) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) - .parent(ids.frame) + .parent(state.ids.frame) .color(UI_MAIN) .label_color(TEXT_COLOR) - .set(ids.online_tab, ui) + .set(state.ids.online_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Online)); @@ -170,16 +206,15 @@ impl<'a> Widget for Social<'a> { // Contents if let SocialTab::Online = self.show.social_tab { - // TODO Needs to be a string sent from the server - // Players list // TODO: this list changes infrequently enough that it should not have to be // recreated every frame - let players = self.client.player_list.values().filter(|p| p.is_online); + let players = self.client.player_list.iter().filter(|(_, p)| p.is_online); let count = players.clone().count(); - if ids.player_names.len() < count { - ids.update(|ids| { - ids.player_names + if state.ids.player_names.len() < count { + state.update(|s| { + s.ids + .player_names .resize(count, &mut ui.widget_id_generator()) }) } @@ -189,25 +224,276 @@ impl<'a> Widget for Social<'a> { .get("hud.social.play_online_fmt") .replace("{nb_player}", &format!("{:?}", count)), ) - .top_left_with_margins_on(ids.content_align, -2.0, 7.0) + .top_left_with_margins_on(state.ids.content_align, -2.0, 7.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(ids.online_title, ui); - for (i, player_info) in players.enumerate() { - Text::new(&format!( - "[{}] {}", - player_info.player_alias, - match &player_info.character { - Some(character) => format!("{} Lvl {}", &character.name, &character.level), - None => "".to_string(), // character select or spectating - } - )) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) + .set(state.ids.online_title, ui); + + // Clear selected player if an entity was selected + if state + .selected_uid + .zip(self.selected_entity) + // Compare instants + .map_or(false, |(u, e)| u.1 < e.1) + { + state.update(|s| s.selected_uid = None); + } + + for (i, (&uid, player_info)) in players.enumerate() { + let selected = state.selected_uid.map_or(false, |u| u.0 == uid); + let alias = &player_info.player_alias; + let character_name_level = match &player_info.character { + Some(character) => format!("{} Lvl {}", &character.name, &character.level), + None => "".to_string(), // character select or spectating + }; + let text = if selected { + format!("-> [{}] {}", alias, character_name_level) + } else { + format!("[{}] {}", alias, character_name_level) + }; + Text::new(&text) + .down(3.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.player_names[i], ui); + // Check for click + if ui + .widget_input(state.ids.player_names[i]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| s.selected_uid = Some((uid, Instant::now()))); + } + } + + Text::new(&self.localized_strings.get("hud.group")) + .down(10.0) + .font_size(self.fonts.cyri.scale(20)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(ids.player_names[i], ui); + .set(state.ids.group, ui); + + // Helper + let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { + Some(player_info) => { + let alias = &player_info.player_alias; + let character_name_level = match &player_info.character { + Some(character) => format!("{} Lvl {}", &character.name, &character.level), + None => "".to_string(), // character select or spectating + }; + format!("[{}] {}", alias, character_name_level) + }, + None => self + .client + .state() + .ecs() + .entity_from_uid(uid.0) + .and_then(|entity| { + self.client + .state() + .ecs() + .read_storage::() + .get(entity) + .map(|stats| stats.name.clone()) + }) + .unwrap_or_else(|| format!("NPC Uid: {}", uid)), + }; + + // Accept/Reject Invite + if let Some(invite_uid) = self.client.group_invite() { + let name = uid_to_name_text(invite_uid, &self.client); + let text = self + .localized_strings + .get("hud.group.invite_to_join") + .replace("{name}", &name); + Text::new(&text) + .down(10.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.group_invite, ui); + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("common.accept")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.accept_invite_button, ui) + .was_clicked() + { + events.push(Event::Accept); + } + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("common.reject")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.reject_invite_button, ui) + .was_clicked() + { + events.push(Event::Reject); + } + } else if self // Invite Button + .client + .group_leader() + .map_or(true, |l_uid| self.client.uid() == Some(l_uid)) + { + let selected = state.selected_uid.map(|s| s.0).or_else(|| { + self.selected_entity + .and_then(|s| self.client.state().read_component_copied(s.0)) + }); + + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.invite")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.invite_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Invite(uid)); + } + } + } + + // Show group members + if let Some(leader) = self.client.group_leader() { + let group_size = self.client.group_members.len() + 1; + if state.ids.member_names.len() < group_size { + state.update(|s| { + s.ids + .member_names + .resize(group_size, &mut ui.widget_id_generator()) + }) + } + // List member names + for (i, &uid) in self + .client + .uid() + .iter() + .chain(self.client.group_members.iter()) + .enumerate() + { + let selected = state.selected_member.map_or(false, |u| u == uid); + let text = uid_to_name_text(uid, &self.client); + let text = if selected { + format!("-> {}", &text) + } else { + text + }; + let text = if uid == leader { + format!("{} (Leader)", &text) + } else { + text + }; + Text::new(&text) + .down(3.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.member_names[i], ui); + // Check for click + if ui + .widget_input(state.ids.member_names[i]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| { + s.selected_member = if selected { None } else { Some(uid) } + }); + } + } + + // Show more buttons if leader + if self.client.uid() == Some(leader) { + let selected = state.selected_member; + // Kick + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.kick")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.kick_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Kick(uid)); + } + } + // Assign leader + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.assign_leader")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.assign_leader_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::AssignLeader(uid)); + } + } + } + + // Leave group button + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.leave")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.leave_button, ui) + .was_clicked() + { + events.push(Event::LeaveGroup); + } } } @@ -229,14 +515,14 @@ impl<'a> Widget for Social<'a> { } else { self.imgs.social_button }) - .right_from(ids.online_tab, 0.0) + .right_from(state.ids.online_tab, 0.0) .label(&self.localized_strings.get("hud.social.friends")) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) - .parent(ids.frame) + .parent(state.ids.frame) .color(UI_MAIN) .label_color(TEXT_COLOR_3) - .set(ids.friends_tab, ui) + .set(state.ids.friends_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Friends)); @@ -246,11 +532,11 @@ impl<'a> Widget for Social<'a> { if let SocialTab::Friends = self.show.social_tab { Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(ids.content_align) + .middle_of(state.ids.content_align) .font_size(self.fonts.cyri.scale(18)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR_3) - .set(ids.friends_test, ui); + .set(state.ids.friends_test, ui); } // Faction Tab @@ -261,14 +547,14 @@ impl<'a> Widget for Social<'a> { }; if Button::image(button_img) .w_h(30.0 * 4.0, 12.0 * 4.0) - .right_from(ids.friends_tab, 0.0) + .right_from(state.ids.friends_tab, 0.0) .label(&self.localized_strings.get("hud.social.faction")) - .parent(ids.frame) + .parent(state.ids.frame) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) .color(UI_MAIN) .label_color(TEXT_COLOR_3) - .set(ids.faction_tab, ui) + .set(state.ids.faction_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Faction)); @@ -278,11 +564,11 @@ impl<'a> Widget for Social<'a> { if let SocialTab::Faction = self.show.social_tab { Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(ids.content_align) + .middle_of(state.ids.content_align) .font_size(self.fonts.cyri.scale(18)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR_3) - .set(ids.faction_test, ui); + .set(state.ids.faction_test, ui); } events diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a6f5e608b1..9efd0c04d1 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -53,7 +53,7 @@ pub struct SessionState { auto_walk: bool, is_aiming: bool, target_entity: Option, - selected_entity: Option, + selected_entity: Option<(specs::Entity, std::time::Instant)>, } /// Represents an active game session (i.e., the one being played). @@ -533,7 +533,8 @@ impl PlayState for SessionState { }, Event::InputUpdate(GameInput::Select, state) => { if !state { - self.selected_entity = self.target_entity; + self.selected_entity = + self.target_entity.map(|e| (e, std::time::Instant::now())); } }, Event::AnalogGameInput(input) => match input { @@ -950,6 +951,24 @@ impl PlayState for SessionState { HudEvent::CraftRecipe(r) => { self.client.borrow_mut().craft_recipe(&r); }, + HudEvent::InviteMember(uid) => { + self.client.borrow_mut().send_group_invite(uid); + }, + HudEvent::AcceptInvite => { + self.client.borrow_mut().accept_group_invite(); + }, + HudEvent::RejectInvite => { + self.client.borrow_mut().reject_group_invite(); + }, + HudEvent::KickMember(uid) => { + self.client.borrow_mut().kick_from_group(uid); + }, + HudEvent::LeaveGroup => { + self.client.borrow_mut().leave_group(); + }, + HudEvent::AssignLeader(uid) => { + self.client.borrow_mut().assign_group_leader(uid); + }, } } From 0a8f148559945df21e4082475cdefb8837809b7c Mon Sep 17 00:00:00 2001 From: Imbris Date: Sat, 11 Jul 2020 23:12:03 -0400 Subject: [PATCH 08/71] Fixes and tweaks for groups --- client/src/lib.rs | 7 ++ common/src/comp/group.rs | 128 +++++++++++++++++++++---------- server/src/events/group_manip.rs | 26 ++++++- server/src/state_ext.rs | 2 +- voxygen/src/hud/mod.rs | 81 +++++++++++++------ voxygen/src/hud/overhead.rs | 9 ++- voxygen/src/hud/social.rs | 9 +++ 7 files changed, 195 insertions(+), 67 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 4347b9af19..8e4f15fe96 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1021,6 +1021,13 @@ impl Client { NewGroup { leader, members } => { self.group_leader = Some(leader); self.group_members = members.into_iter().collect(); + // Currently add/remove messages treat client as an implicit member + // of the group whereas this message explicitly included them so to + // be consistent for now we will remove the client from the + // received hashset + if let Some(uid) = self.uid() { + self.group_members.remove(&uid); + } }, NoGroup => { self.group_leader = None; diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 99f2167cc3..e843f32720 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -89,7 +89,9 @@ fn with_pets( ) -> Vec { let mut list = (entities, alignments) .join() - .filter_map(|(e, a)| matches!(a, Alignment::Owned(owner) if *owner == uid).then_some(e)) + .filter_map(|(e, a)| { + matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e) + }) .collect::>(); list.push(entity); list @@ -145,8 +147,12 @@ impl GroupManager { }; // If new member is a member of a different group remove that - if groups.get(new_member).is_some() { - self.remove_from_group( + if groups + .get(new_member) + .and_then(|g| self.group_info(*g)) + .is_some() + { + self.leave_group( new_member, groups, alignments, @@ -168,7 +174,7 @@ impl GroupManager { // Member of an existing group can't be a leader // If the lead is a member of another group leave that group first Some(_) => { - self.remove_from_group(leader, groups, alignments, uids, entities, &mut notifier); + self.leave_group(leader, groups, alignments, uids, entities, &mut notifier); None }, None => None, @@ -238,10 +244,7 @@ impl GroupManager { } } - // Remove someone from a group if they are in one - // Don't need to check if they are in a group before calling this - // Also removes pets (ie call this if the pet no longer exists) - pub fn remove_from_group( + pub fn leave_group( &mut self, member: specs::Entity, groups: &mut GroupsMut, @@ -249,6 +252,52 @@ impl GroupManager { uids: &Uids, entities: &specs::Entities, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), + ) { + // Pets can't leave + if matches!(alignments.get(member), Some(Alignment::Owned(uid)) if uids.get(member).map_or(true, |u| u != uid)) + { + return; + } + self.remove_from_group(member, groups, alignments, uids, entities, notifier, false); + + // Set NPC back to their group + if let Some(alignment) = alignments.get(member) { + match alignment { + Alignment::Npc => { + let _ = groups.insert(member, NPC); + }, + Alignment::Enemy => { + let _ = groups.insert(member, ENEMY); + }, + _ => {}, + } + } + } + + pub fn entity_deleted( + &mut self, + member: specs::Entity, + groups: &mut GroupsMut, + alignments: &Alignments, + uids: &Uids, + entities: &specs::Entities, + notifier: &mut impl FnMut(specs::Entity, ChangeNotification), + ) { + self.remove_from_group(member, groups, alignments, uids, entities, notifier, true); + } + + // Remove someone from a group if they are in one + // Don't need to check if they are in a group before calling this + // Also removes pets (ie call this if the pet no longer exists) + fn remove_from_group( + &mut self, + member: specs::Entity, + groups: &mut GroupsMut, + alignments: &Alignments, + uids: &Uids, + entities: &specs::Entities, + notifier: &mut impl FnMut(specs::Entity, ChangeNotification), + to_be_deleted: bool, ) { let group = match groups.get(member) { Some(group) => *group, @@ -266,11 +315,14 @@ impl GroupManager { (entities, uids, &*groups, alignments.maybe()) .join() - .filter(|(_, _, g, _)| **g == group) + .filter(|(e, _, g, _)| **g == group && (!to_be_deleted || *e == member)) .fold( HashMap::, Vec)>::new(), |mut acc, (e, uid, _, alignment)| { - if let Some(Alignment::Owned(owner)) = alignment { + if let Some(owner) = alignment.and_then(|a| match a { + Alignment::Owned(owner) if uid != owner => Some(owner), + _ => None, + }) { // Assumes owner will be in the group acc.entry(*owner).or_default().1.push(e); } else { @@ -309,10 +361,6 @@ impl GroupManager { notifier(owner, ChangeNotification::NoGroup) } } else { - warn!( - "Something went wrong! The pet owner is missing from a group that the \ - pet is in" - ); pets.into_iter() .for_each(|pet| notifier(pet, ChangeNotification::NoGroup)); } @@ -328,47 +376,45 @@ impl GroupManager { let leaving = with_pets(member, leaving_member_uid, alignments, entities); - // If pets form new group - if leaving.len() > 1 { + // If pets and not about to be deleted form new group + if leaving.len() > 1 && !to_be_deleted { let new_group = self.create_group(member); - leaving.iter().for_each(|e| { - let _ = groups.insert(*e, new_group).unwrap(); - }); - let notification = ChangeNotification::NewGroup { leader: member, members: leaving.clone(), }; - leaving - .iter() - .for_each(|e| notifier(*e, notification.clone())); + leaving.iter().for_each(|&e| { + let _ = groups.insert(e, new_group).unwrap(); + notifier(e, notification.clone()); + }); } else { - groups.remove(member); - notifier(member, ChangeNotification::NoGroup) + leaving.iter().for_each(|&e| { + let _ = groups.remove(e); + notifier(e, ChangeNotification::NoGroup); + }); } - // Inform remaining members - let mut num_members = 0; - members(group, &*groups, entities).for_each(|a| { - num_members += 1; - leaving.iter().for_each(|b| { - notifier(a, ChangeNotification::Removed(*b)); - }) - }); - - // If leader is the last one left then disband the group - // Assumes last member is the leader - if num_members == 1 { - if let Some(info) = self.group_info(group) { + if let Some(info) = self.group_info(group) { + // Inform remaining members + let mut num_members = 0; + members(group, &*groups, entities).for_each(|a| { + num_members += 1; + leaving.iter().for_each(|b| { + notifier(a, ChangeNotification::Removed(*b)); + }) + }); + // If leader is the last one left then disband the group + // Assumes last member is the leader + if num_members == 1 { let leader = info.leader; self.remove_group(group); groups.remove(leader); notifier(leader, ChangeNotification::NoGroup); + } else if num_members == 0 { + error!("Somehow group has no members") } - } else if num_members == 0 { - error!("Somehow group has no members") } } } @@ -392,7 +438,7 @@ impl GroupManager { // Point to new leader members(group, groups, entities).for_each(|e| { - notifier(e, ChangeNotification::NewLeader(e)); + notifier(e, ChangeNotification::NewLeader(new_leader)); }); } } diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index 2c6447541b..bfc15bb946 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -153,7 +153,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani let mut clients = state.ecs().write_storage::(); let uids = state.ecs().read_storage::(); let mut group_manager = state.ecs().write_resource::(); - group_manager.remove_from_group( + group_manager.leave_group( entity, &mut state.ecs().write_storage(), &state.ecs().read_storage(), @@ -174,6 +174,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani GroupManip::Kick(uid) => { let mut clients = state.ecs().write_storage::(); let uids = state.ecs().read_storage::(); + let alignments = state.ecs().read_storage::(); let target = match state.ecs().entity_from_uid(uid.into()) { Some(t) => t, @@ -188,6 +189,27 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani return; }, }; + + // Can't kick pet + if matches!(alignments.get(target), Some(comp::Alignment::Owned(owner)) if uids.get(target).map_or(true, |u| u != owner)) + { + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg("Kick failed, can't kick pet".to_owned()), + ); + } + return; + } + // Can't kick yourself + if uids.get(entity).map_or(false, |u| *u == uid) { + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg("Kick failed, can't kick yourself".to_owned()), + ); + } + return; + } + let mut groups = state.ecs().write_storage::(); let mut group_manager = state.ecs().write_resource::(); // Make sure kicker is the group leader @@ -197,7 +219,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani { Some(info) if info.leader == entity => { // Remove target from group - group_manager.remove_from_group( + group_manager.leave_group( target, &mut groups, &state.ecs().read_storage(), diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index cdce5d44d3..20a3189efc 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -351,7 +351,7 @@ impl StateExt for State { let mut clients = self.ecs().write_storage::(); let uids = self.ecs().read_storage::(); let mut group_manager = self.ecs().write_resource::(); - group_manager.remove_from_group( + group_manager.entity_deleted( entity, &mut self.ecs().write_storage(), &self.ecs().read_storage(), diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 93caf26a22..079a6a5d40 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -117,6 +117,8 @@ const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0); //const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0); +/// Distance at which nametags are visible for group members +const NAMETAG_GROUP_RANGE: f32 = 300.0; /// Distance at which nametags are visible const NAMETAG_RANGE: f32 = 40.0; /// Time nametags stay visible after doing damage even if they are out of range @@ -1052,7 +1054,7 @@ impl Hud { } // Render overhead name tags and health bars - for (pos, name, stats, energy, height_offset, hpfl, uid) in ( + for (pos, name, stats, energy, height_offset, hpfl, uid, in_group) in ( &entities, &pos, interpolated.maybe(), @@ -1065,15 +1067,34 @@ impl Hud { &uids, ) .join() - .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead + .map(|(a, b, c, d, e, f, g, h, i, uid)| { + ( + a, + b, + c, + d, + e, + f, + g, + h, + i, + uid, + client.group_members.contains(uid), + ) + }) + .filter(|(entity, pos, _, stats, _, _, _, _, hpfl, uid, in_group)| { + *entity != me && !stats.is_dead && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity) || info.selected_entity.map_or(false, |s| s.0 == *entity) - )) - // Don't show outside a certain range - .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { - pos.0.distance_squared(player_pos) - < (if hpfl + || *in_group + ) + // Don't show outside a certain range + && pos.0.distance_squared(player_pos) + < (if *in_group + { + NAMETAG_GROUP_RANGE + } else if hpfl .time_since_last_dmg_by_me .map_or(false, |t| t < NAMETAG_DMG_TIME) { @@ -1083,25 +1104,40 @@ impl Hud { }) .powi(2) }) - .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, uid)| { - // TODO: This is temporary - // If the player used the default character name display their name instead - let name = if stats.name == "Character Name" { - player.map_or(&stats.name, |p| &p.alias) - } else { - &stats.name - }; - ( - interpolated.map_or(pos.0, |i| i.pos), - name, + .map( + |( + _, + pos, + interpolated, stats, energy, - // TODO: when body.height() is more accurate remove the 2.0 - body.height() * 2.0 * scale.map_or(1.0, |s| s.0), + player, + scale, + body, hpfl, uid, - ) - }) + in_group, + )| { + // TODO: This is temporary + // If the player used the default character name display their name instead + let name = if stats.name == "Character Name" { + player.map_or(&stats.name, |p| &p.alias) + } else { + &stats.name + }; + ( + interpolated.map_or(pos.0, |i| i.pos), + name, + stats, + energy, + // TODO: when body.height() is more accurate remove the 2.0 + body.height() * 2.0 * scale.map_or(1.0, |s| s.0), + hpfl, + uid, + in_group, + ) + }, + ) { let bubble = self.speech_bubbles.get(uid); @@ -1118,6 +1154,7 @@ impl Hud { stats, energy, own_level, + in_group, &global_state.settings.gameplay, self.pulse, &self.voxygen_i18n, diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index ad551d7281..7f54b8d6fd 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -56,6 +56,7 @@ pub struct Overhead<'a> { stats: &'a Stats, energy: Option<&'a Energy>, own_level: u32, + in_group: bool, settings: &'a GameplaySettings, pulse: f32, voxygen_i18n: &'a std::sync::Arc, @@ -73,6 +74,7 @@ impl<'a> Overhead<'a> { stats: &'a Stats, energy: Option<&'a Energy>, own_level: u32, + in_group: bool, settings: &'a GameplaySettings, pulse: f32, voxygen_i18n: &'a std::sync::Arc, @@ -85,6 +87,7 @@ impl<'a> Overhead<'a> { stats, energy, own_level, + in_group, settings, pulse, voxygen_i18n, @@ -145,7 +148,11 @@ impl<'a> Widget for Overhead<'a> { Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) .font_size(30) - .color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) + .color(if self.in_group { + Color::Rgba(1.0, 0.5, 0.6, 1.0) + } else { + Color::Rgba(0.61, 0.61, 0.89, 1.0) + }) .x_y(0.0, MANA_BAR_Y + 50.0) .set(state.ids.name, ui); diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index cfbe8b9e7b..a13532f2a0 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -375,6 +375,9 @@ impl<'a> Widget for Social<'a> { { if let Some(uid) = selected { events.push(Event::Invite(uid)); + state.update(|s| { + s.selected_uid = None; + }); } } } @@ -452,6 +455,9 @@ impl<'a> Widget for Social<'a> { { if let Some(uid) = selected { events.push(Event::Kick(uid)); + state.update(|s| { + s.selected_member = None; + }); } } // Assign leader @@ -474,6 +480,9 @@ impl<'a> Widget for Social<'a> { { if let Some(uid) = selected { events.push(Event::AssignLeader(uid)); + state.update(|s| { + s.selected_member = None; + }); } } } From ef4b221706c33f824b0cf88b8d4888cffbbe5d97 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 12 Jul 2020 00:04:13 -0400 Subject: [PATCH 09/71] Make entity targeting easier, add EXP sharing --- server/src/events/entity_manipulation.rs | 44 +++++++++++++++++++----- voxygen/src/hud/mod.rs | 2 +- voxygen/src/session.rs | 2 +- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 8033ccebdb..bbfe1488f1 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2,8 +2,8 @@ use crate::{client::Client, Server, SpawnPoint, StateExt}; use common::{ assets, comp::{ - self, item::lottery::Lottery, object, Body, Damage, DamageSource, HealthChange, - HealthSource, Player, Stats, + self, item::lottery::Lottery, object, Body, Damage, DamageSource, Group, HealthChange, + HealthSource, Player, Pos, Stats, }, msg::{PlayerListUpdate, ServerMsg}, state::BlockChange, @@ -62,16 +62,44 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } = cause { + const MAX_EXP_DIST: f32 = 150.0; + // Attacker gets double exp of everyone else + const ATTACKER_EXP_WEIGHT: f32 = 2.0; + let mut exp_reward = (entity_stats.body_type.base_exp() + + entity_stats.level.level() * entity_stats.body_type.base_exp_increase()) + as f32; state.ecs().entity_from_uid(by.into()).map(|attacker| { + // Distribute EXP to group + let groups = state.ecs().read_storage::(); + let positions = state.ecs().read_storage::(); + // TODO: rework if change to groups makes it easier to iterate entities in a + // group + if let (Some(attacker_group), Some(pos)) = + (groups.get(attacker), positions.get(entity)) + { + let members_in_range = (&state.ecs().entities(), &groups, &positions) + .join() + .filter(|(entity, group, member_pos)| { + *group == attacker_group + && *entity != attacker + && pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) + }) + .map(|(entity, _, _)| entity) + .collect::>(); + let exp = + exp_reward / (members_in_range.len() as f32 + ATTACKER_EXP_WEIGHT); + exp_reward = exp * ATTACKER_EXP_WEIGHT; + members_in_range.into_iter().for_each(|e| { + if let Some(stats) = stats.get_mut(e) { + stats.exp.change_by(exp.ceil() as i64); + } + }); + } + if let Some(attacker_stats) = stats.get_mut(attacker) { // TODO: Discuss whether we should give EXP by Player // Killing or not. - attacker_stats.exp.change_by( - (entity_stats.body_type.base_exp() - + entity_stats.level.level() - * entity_stats.body_type.base_exp_increase()) - as i64, - ); + attacker_stats.exp.change_by(exp_reward.ceil() as i64); } }); } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 079a6a5d40..01293cee13 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1082,7 +1082,7 @@ impl Hud { client.group_members.contains(uid), ) }) - .filter(|(entity, pos, _, stats, _, _, _, _, hpfl, uid, in_group)| { + .filter(|(entity, pos, _, stats, _, _, _, _, hpfl, _, in_group)| { *entity != me && !stats.is_dead && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity) diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 9efd0c04d1..b37af8c2ee 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1123,7 +1123,7 @@ fn under_cursor( .join() .filter(|(e, _, _, _)| *e != player_entity) .map(|(e, p, s, b)| { - const RADIUS_SCALE: f32 = 1.2; + const RADIUS_SCALE: f32 = 1.6; let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; // Move position up from the feet let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); From d856c202253df75c6f224b92819be70b9fedb093 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 12 Jul 2020 16:18:57 -0400 Subject: [PATCH 10/71] Integrate groups with chat groups --- common/src/cmd.rs | 16 ++++----- common/src/comp/chat.rs | 75 ++++++++++++++++++++++++--------------- common/src/comp/group.rs | 12 ++++--- common/src/comp/mod.rs | 3 +- common/src/event.rs | 2 +- common/src/state.rs | 1 - common/src/sys/agent.rs | 9 ++--- server/src/cmd.rs | 13 +++---- server/src/state_ext.rs | 32 ++++++++++------- server/src/sys/message.rs | 8 ++--- voxygen/src/hud/chat.rs | 2 +- 11 files changed, 101 insertions(+), 72 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 812af83795..04ead5547d 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -50,7 +50,7 @@ pub enum ChatCommand { Health, Help, JoinFaction, - JoinGroup, + //JoinGroup, Jump, Kill, KillNpcs, @@ -92,7 +92,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Health, ChatCommand::Help, ChatCommand::JoinFaction, - ChatCommand::JoinGroup, + //ChatCommand::JoinGroup, ChatCommand::Jump, ChatCommand::Kill, ChatCommand::KillNpcs, @@ -246,11 +246,11 @@ impl ChatCommand { "Join/leave the specified faction", NoAdmin, ), - ChatCommand::JoinGroup => ChatCommandData::new( - vec![Any("group", Optional)], - "Join/leave the specified group", - NoAdmin, - ), + //ChatCommand::JoinGroup => ChatCommandData::new( + // vec![Any("group", Optional)], + // "Join/leave the specified group", + // NoAdmin, + //), ChatCommand::Jump => cmd( vec![ Float("x", 0.0, Required), @@ -383,7 +383,7 @@ impl ChatCommand { ChatCommand::Group => "group", ChatCommand::Health => "health", ChatCommand::JoinFaction => "join_faction", - ChatCommand::JoinGroup => "join_group", + //ChatCommand::JoinGroup => "join_group", ChatCommand::Help => "help", ChatCommand::Jump => "jump", ChatCommand::Kill => "kill", diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index 589d808ad8..019b1876bd 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -1,4 +1,4 @@ -use crate::{msg::ServerMsg, sync::Uid}; +use crate::{comp::group::Group, msg::ServerMsg, sync::Uid}; use serde::{Deserialize, Serialize}; use specs::Component; use specs_idvs::IdvStorage; @@ -15,7 +15,7 @@ pub enum ChatMode { /// Talk to players in your region of the world Region, /// Talk to your current group of players - Group(String), + Group(Group), /// Talk to your faction Faction(String), /// Talk to every player on the server @@ -28,16 +28,16 @@ impl Component for ChatMode { impl ChatMode { /// Create a message from your current chat mode and uuid. - pub fn new_message(&self, from: Uid, message: String) -> ChatMsg { + pub fn new_message(&self, from: Uid, message: String) -> UnresolvedChatMsg { let chat_type = match self { ChatMode::Tell(to) => ChatType::Tell(from, *to), ChatMode::Say => ChatType::Say(from), ChatMode::Region => ChatType::Region(from), - ChatMode::Group(name) => ChatType::Group(from, name.to_string()), - ChatMode::Faction(name) => ChatType::Faction(from, name.to_string()), + ChatMode::Group(group) => ChatType::Group(from, *group), + ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()), ChatMode::World => ChatType::World(from), }; - ChatMsg { chat_type, message } + UnresolvedChatMsg { chat_type, message } } } @@ -49,7 +49,7 @@ impl Default for ChatMode { /// /// This is a superset of `SpeechBubbleType`, which is a superset of `ChatMode` #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ChatType { +pub enum ChatType { /// A player came online Online, /// A player went offline @@ -61,7 +61,7 @@ pub enum ChatType { /// Inform players that someone died Kill, /// Server notifications to a group, such as player join/leave - GroupMeta(String), + GroupMeta(G), /// Server notifications to a faction, such as player join/leave FactionMeta(String), /// One-on-one chat (from, to) @@ -69,7 +69,7 @@ pub enum ChatType { /// Chat with nearby players Say(Uid), /// Group chat - Group(Uid, String), + Group(Uid, G), /// Factional chat Faction(Uid, String), /// Regional chat @@ -86,17 +86,18 @@ pub enum ChatType { Loot, } -impl ChatType { - pub fn chat_msg(self, msg: S) -> ChatMsg +impl ChatType { + pub fn chat_msg(self, msg: S) -> GenericChatMsg where S: Into, { - ChatMsg { + GenericChatMsg { chat_type: self, message: msg.into(), } } - +} +impl ChatType { pub fn server_msg(self, msg: S) -> ServerMsg where S: Into, @@ -106,12 +107,15 @@ impl ChatType { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatMsg { - pub chat_type: ChatType, +pub struct GenericChatMsg { + pub chat_type: ChatType, pub message: String, } -impl ChatMsg { +pub type ChatMsg = GenericChatMsg; +pub type UnresolvedChatMsg = GenericChatMsg; + +impl GenericChatMsg { pub const NPC_DISTANCE: f32 = 100.0; pub const REGION_DISTANCE: f32 = 1000.0; pub const SAY_DISTANCE: f32 = 100.0; @@ -121,6 +125,32 @@ impl ChatMsg { Self { chat_type, message } } + pub fn map_group(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg { + let chat_type = match self.chat_type { + ChatType::Online => ChatType::Online, + ChatType::Offline => ChatType::Offline, + ChatType::CommandInfo => ChatType::CommandInfo, + ChatType::CommandError => ChatType::CommandError, + ChatType::Loot => ChatType::Loot, + ChatType::FactionMeta(a) => ChatType::FactionMeta(a), + ChatType::GroupMeta(g) => ChatType::GroupMeta(f(g)), + ChatType::Kill => ChatType::Kill, + ChatType::Tell(a, b) => ChatType::Tell(a, b), + ChatType::Say(a) => ChatType::Say(a), + ChatType::Group(a, g) => ChatType::Group(a, f(g)), + ChatType::Faction(a, b) => ChatType::Faction(a, b), + ChatType::Region(a) => ChatType::Region(a), + ChatType::World(a) => ChatType::World(a), + ChatType::Npc(a, b) => ChatType::Npc(a, b), + ChatType::Meta => ChatType::Meta, + }; + + GenericChatMsg { + chat_type, + message: self.message, + } + } + pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> { let icon = self.icon(); if let ChatType::Npc(from, r) = self.chat_type { @@ -174,19 +204,6 @@ impl ChatMsg { } } -/// Player groups are useful when forming raiding parties and coordinating -/// gameplay. -/// -/// Groups are currently just an associated String (the group's name) -#[derive(Clone, Debug)] -pub struct Group(pub String); -impl Component for Group { - type Storage = IdvStorage; -} -impl From for Group { - fn from(s: String) -> Self { Group(s) } -} - /// Player factions are used to coordinate pvp vs hostile factions or segment /// chat from the world /// diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index e843f32720..acbcf17fa1 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -27,11 +27,12 @@ impl Component for Group { type Storage = FlaggedStorage>; } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct GroupInfo { // TODO: what about enemy groups, either the leader will constantly change because they have to // be loaded or we create a dummy entity or this needs to be optional pub leader: specs::Entity, + pub name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -110,12 +111,15 @@ pub fn members<'a>( // TODO: optimize add/remove for massive NPC groups impl GroupManager { - pub fn group_info(&self, group: Group) -> Option { - self.groups.get(group.0 as usize).copied() + pub fn group_info(&self, group: Group) -> Option<&GroupInfo> { + self.groups.get(group.0 as usize) } fn create_group(&mut self, leader: specs::Entity) -> Group { - Group(self.groups.insert(GroupInfo { leader }) as u32) + Group(self.groups.insert(GroupInfo { + leader, + name: "Flames".into(), + }) as u32) } fn remove_group(&mut self, group: Group) { self.groups.remove(group.0 as usize); } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 041a789b37..ccef80dc8b 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -29,9 +29,8 @@ pub use body::{ humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData, }; pub use character_state::{Attacking, CharacterState, StateUpdate}; -// TODO: replace chat::Group functionality with group::Group pub use chat::{ - ChatMode, ChatMsg, ChatType, Faction, Group as ChatGroup, SpeechBubble, SpeechBubbleType, + ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, }; pub use controller::{ Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input, diff --git a/common/src/event.rs b/common/src/event.rs index 1e2ba17543..9a07615c68 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -81,7 +81,7 @@ pub enum ServerEvent { ChunkRequest(EcsEntity, Vec2), ChatCmd(EcsEntity, String), /// Send a chat message to the player from an npc or other player - Chat(comp::ChatMsg), + Chat(comp::UnresolvedChatMsg), } pub struct EventBus { diff --git a/common/src/state.rs b/common/src/state.rs index 9d5a992e04..a76698c944 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -157,7 +157,6 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); ecs.register::(); // Register synced resources used by the ECS. diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 4e0690ccac..5f3fa14869 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -4,8 +4,8 @@ use crate::{ agent::Activity, group, item::{tool::ToolKind, ItemKind}, - Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout, - MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel, + Agent, Alignment, Body, CharacterState, ControlAction, Controller, Loadout, MountState, + Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, path::{Chaser, TraversalConfig}, @@ -430,8 +430,9 @@ impl<'a> System<'a> for Sys { if stats.get(attacker).map_or(false, |a| !a.is_dead) { if agent.can_speak { let msg = "npc.speech.villager_under_attack".to_string(); - event_bus - .emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg))); + event_bus.emit_now(ServerEvent::Chat( + UnresolvedChatMsg::npc(*uid, msg), + )); } agent.activity = Activity::Attack { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 344f7c66b0..eb14d24e24 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -77,7 +77,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Health => handle_health, ChatCommand::Help => handle_help, ChatCommand::JoinFaction => handle_join_faction, - ChatCommand::JoinGroup => handle_join_group, + //ChatCommand::JoinGroup => handle_join_group, ChatCommand::Jump => handle_jump, ChatCommand::Kill => handle_kill, ChatCommand::KillNpcs => handle_kill_npcs, @@ -1197,8 +1197,8 @@ fn handle_group( return; } let ecs = server.state.ecs(); - if let Some(comp::ChatGroup(group)) = ecs.read_storage().get(client) { - let mode = comp::ChatMode::Group(group.to_string()); + if let Some(group) = ecs.read_storage::().get(client) { + let mode = comp::ChatMode::Group(*group); let _ = ecs.write_storage().insert(client, mode.clone()); if !msg.is_empty() { if let Some(uid) = ecs.read_storage().get(client) { @@ -1208,7 +1208,7 @@ fn handle_group( } else { server.notify_client( client, - ChatType::CommandError.server_msg("Please join a group with /join_group"), + ChatType::CommandError.server_msg("Please create a group first"), ); } } @@ -1359,7 +1359,8 @@ fn handle_join_faction( } } -fn handle_join_group( +// TODO: it might be useful to copy the GroupMeta messages elsewhere +/*fn handle_join_group( server: &mut Server, client: EcsEntity, target: EcsEntity, @@ -1419,7 +1420,7 @@ fn handle_join_group( ChatType::CommandError.server_msg("Could not find your player alias"), ); } -} +}*/ #[cfg(not(feature = "worldgen"))] fn handle_debug_column( diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 20a3189efc..d50b5ef892 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -46,7 +46,7 @@ pub trait StateExt { /// Performed after loading component data from the database fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); /// Iterates over registered clients and send each `ServerMsg` - fn send_chat(&self, msg: comp::ChatMsg); + fn send_chat(&self, msg: comp::UnresolvedChatMsg); fn notify_registered_clients(&self, msg: ServerMsg); /// Delete an entity, recording the deletion in [`DeletedEntities`] fn delete_entity_recorded( @@ -240,10 +240,18 @@ impl StateExt for State { /// Send the chat message to the proper players. Say and region are limited /// by location. Faction and group are limited by component. - fn send_chat(&self, msg: comp::ChatMsg) { + fn send_chat(&self, msg: comp::UnresolvedChatMsg) { let ecs = self.ecs(); let is_within = |target, a: &comp::Pos, b: &comp::Pos| a.0.distance_squared(b.0) < target * target; + + let group_manager = ecs.read_resource::(); + let resolved_msg = msg.clone().map_group(|group_id| { + group_manager + .group_info(group_id) + .map_or_else(|| "???".into(), |i| i.name.clone()) + }); + match &msg.chat_type { comp::ChatType::Online | comp::ChatType::Offline @@ -253,7 +261,7 @@ impl StateExt for State { | comp::ChatType::Kill | comp::ChatType::Meta | comp::ChatType::World(_) => { - self.notify_registered_clients(ServerMsg::ChatMsg(msg.clone())) + self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg.clone())) }, comp::ChatType::Tell(u, t) => { for (client, uid) in ( @@ -263,7 +271,7 @@ impl StateExt for State { .join() { if uid == u || uid == t { - client.notify(ServerMsg::ChatMsg(msg.clone())); + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } }, @@ -275,7 +283,7 @@ impl StateExt for State { if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) { for (client, pos) in (&mut ecs.write_storage::(), &positions).join() { if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) { - client.notify(ServerMsg::ChatMsg(msg.clone())); + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } } @@ -287,7 +295,7 @@ impl StateExt for State { if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) { for (client, pos) in (&mut ecs.write_storage::(), &positions).join() { if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) { - client.notify(ServerMsg::ChatMsg(msg.clone())); + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } } @@ -299,7 +307,7 @@ impl StateExt for State { if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) { for (client, pos) in (&mut ecs.write_storage::(), &positions).join() { if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) { - client.notify(ServerMsg::ChatMsg(msg.clone())); + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } } @@ -313,19 +321,19 @@ impl StateExt for State { .join() { if s == &faction.0 { - client.notify(ServerMsg::ChatMsg(msg.clone())); + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } }, - comp::ChatType::GroupMeta(s) | comp::ChatType::Group(_, s) => { + comp::ChatType::GroupMeta(g) | comp::ChatType::Group(_, g) => { for (client, group) in ( &mut ecs.write_storage::(), - &ecs.read_storage::(), + &ecs.read_storage::(), ) .join() { - if s == &group.0 { - client.notify(ServerMsg::ChatMsg(msg.clone())); + if g == group { + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } }, diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index d2d7c895d6..759c469004 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -5,7 +5,7 @@ use crate::{ }; use common::{ comp::{ - Admin, AdminList, CanBuild, ChatMode, ChatMsg, ChatType, ControlEvent, Controller, + Admin, AdminList, CanBuild, ChatMode, UnresolvedChatMsg, ChatType, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, Stats, Vel, }, event::{EventBus, ServerEvent}, @@ -32,7 +32,7 @@ impl Sys { #[allow(clippy::too_many_arguments)] async fn handle_client_msg( server_emitter: &mut common::event::Emitter<'_, ServerEvent>, - new_chat_msgs: &mut Vec<(Option, ChatMsg)>, + new_chat_msgs: &mut Vec<(Option, UnresolvedChatMsg)>, player_list: &HashMap, new_players: &mut Vec, entity: specs::Entity, @@ -202,7 +202,7 @@ impl Sys { // Only send login message if it wasn't already // sent previously if !client.login_msg_sent { - new_chat_msgs.push((None, ChatMsg { + new_chat_msgs.push((None, UnresolvedChatMsg { chat_type: ChatType::Online, message: format!("[{}] is now online.", &player.alias), // TODO: Localize this })); @@ -461,7 +461,7 @@ impl<'a> System<'a> for Sys { let mut server_emitter = server_event_bus.emitter(); - let mut new_chat_msgs: Vec<(Option, ChatMsg)> = Vec::new(); + let mut new_chat_msgs = Vec::new(); // Player list to send new players. let player_list = (&uids, &players, stats.maybe(), admins.maybe()) diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 46267014ad..7b5ac61548 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -475,7 +475,7 @@ fn cursor_offset_to_index( } /// Get the color and icon for the current line in the chat box -fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) { +fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) { match chat_type { ChatType::Online => (ONLINE_COLOR, imgs.chat_online_small), ChatType::Offline => (OFFLINE_COLOR, imgs.chat_offline_small), From 9cffb614296b4209b9482400db22d7f55d95f4bb Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Sun, 12 Jul 2020 13:10:26 +0200 Subject: [PATCH 11/71] colors --- voxygen/src/hud/mod.rs | 4 ++++ voxygen/src/hud/overhead.rs | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 01293cee13..773e4897e9 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -111,6 +111,10 @@ const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0); /// Color for collected loot messages const LOOT_COLOR: Color = Color::Rgba(0.69, 0.57, 1.0, 1.0); +//Nametags +const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); +const DEFAULT_NPC: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); + // UI Color-Theme const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue //const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index 7f54b8d6fd..abcf9ebdd5 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -1,6 +1,6 @@ use super::{ - img_ids::Imgs, FACTION_COLOR, GROUP_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, REGION_COLOR, - SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR, + img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR, + MANA_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR, }; use crate::{ i18n::VoxygenLocalization, @@ -149,9 +149,9 @@ impl<'a> Widget for Overhead<'a> { .font_id(self.fonts.cyri.conrod_id) .font_size(30) .color(if self.in_group { - Color::Rgba(1.0, 0.5, 0.6, 1.0) + GROUP_MEMBER } else { - Color::Rgba(0.61, 0.61, 0.89, 1.0) + DEFAULT_NPC }) .x_y(0.0, MANA_BAR_Y + 50.0) .set(state.ids.name, ui); From 3a22b3694d6eb71426ecf3b80f97df7490a20856 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 19 Jul 2020 17:49:18 -0400 Subject: [PATCH 12/71] New group UI functions --- assets/voxygen/i18n/en.ron | 4 +- client/src/lib.rs | 119 ++++---- common/src/comp/controller.rs | 2 +- common/src/comp/group.rs | 152 ++++++---- server/src/cmd.rs | 2 + server/src/events/group_manip.rs | 8 +- server/src/events/inventory_manip.rs | 2 + server/src/events/player.rs | 2 + server/src/sys/message.rs | 4 +- voxygen/src/hud/group.rs | 420 ++++++++++++++++----------- voxygen/src/hud/mod.rs | 15 +- voxygen/src/hud/social.rs | 230 +-------------- voxygen/src/session.rs | 4 +- voxygen/src/settings.rs | 6 +- voxygen/src/window.rs | 4 + 15 files changed, 440 insertions(+), 534 deletions(-) diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index a52a535254..1a067a3229 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -65,7 +65,7 @@ VoxygenLocalization( "common.create": "Create", "common.okay": "Okay", "common.accept": "Accept", - "common.reject": "Reject", + "common.decline": "Decline", "common.disclaimer": "Disclaimer", "common.cancel": "Cancel", "common.none": "None", @@ -386,6 +386,8 @@ magically infused items?"#, "gameinput.autowalk": "Auto Walk", "gameinput.dance": "Dance", "gameinput.select": "Select Entity", + "gameinput.acceptgroupinvite": "Accept Group Invite", + "gameinput.declinegroupinvite": "Decline Group Invite", /// End GameInput section diff --git a/client/src/lib.rs b/client/src/lib.rs index 8e4f15fe96..6c861c7ee2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -18,7 +18,7 @@ use common::{ character::CharacterItem, comp::{ self, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, - InventoryManip, InventoryUpdateEvent, + InventoryManip, InventoryUpdateEvent, group, }, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, @@ -34,7 +34,7 @@ use common::{ use futures_executor::block_on; use futures_timer::Delay; use futures_util::{select, FutureExt}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashMap; use image::DynamicImage; use network::{ Network, Participant, Pid, ProtocolAddr, Stream, PROMISES_CONSISTENCY, PROMISES_ORDERED, @@ -74,7 +74,6 @@ pub struct Client { pub server_info: ServerInfo, pub world_map: (Arc, Vec2), pub player_list: HashMap, - pub group_members: HashSet, pub character_list: CharacterList, pub active_character_id: Option, recipe_book: RecipeBook, @@ -82,6 +81,7 @@ pub struct Client { group_invite: Option, group_leader: Option, + group_members: HashMap, _network: Network, participant: Option, @@ -212,7 +212,7 @@ impl Client { server_info, world_map, player_list: HashMap::new(), - group_members: HashSet::new(), + group_members: HashMap::new(), character_list: CharacterList::default(), active_character_id: None, recipe_book, @@ -434,11 +434,15 @@ impl Client { pub fn group_invite(&self) -> Option { self.group_invite } - pub fn group_leader(&self) -> Option { self.group_leader } + pub fn group_info(&self) -> Option<(String, Uid)> { self.group_leader.map(|l| ("TODO".into(), l)) } + + pub fn group_members(&self) -> &HashMap { &self.group_members } pub fn send_group_invite(&mut self, invitee: Uid) { self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Invite(invitee) ))) + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Invite(invitee), + ))) .unwrap() } @@ -448,37 +452,42 @@ impl Client { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Accept, - ))).unwrap(); + ))) + .unwrap(); } - pub fn reject_group_invite(&mut self) { + pub fn decline_group_invite(&mut self) { // Clear invite self.group_invite.take(); self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::Reject, - ))).unwrap(); + GroupManip::Decline, + ))) + .unwrap(); } pub fn leave_group(&mut self) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Leave, - ))).unwrap(); + ))) + .unwrap(); } pub fn kick_from_group(&mut self, uid: Uid) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Kick(uid), - ))).unwrap(); + ))) + .unwrap(); } pub fn assign_group_leader(&mut self, uid: Uid) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::AssignLeader(uid), - ))).unwrap(); + ))) + .unwrap(); } pub fn is_mounted(&self) -> bool { @@ -993,47 +1002,47 @@ impl Client { } }, ServerMsg::GroupUpdate(change_notification) => { - use comp::group::ChangeNotification::*; - // Note: we use a hashmap since this would not work with entities outside - // the view distance - match change_notification { - Added(uid) => { - if !self.group_members.insert(uid) { - warn!( - "Received msg to add uid {} to the group members but they \ - were already there", - uid - ); - } - }, - Removed(uid) => { - if !self.group_members.remove(&uid) { - warn!( - "Received msg to remove uid {} from group members but by \ - they weren't in there!", - uid - ); - } - }, - NewLeader(leader) => { - self.group_leader = Some(leader); - }, - NewGroup { leader, members } => { - self.group_leader = Some(leader); - self.group_members = members.into_iter().collect(); - // Currently add/remove messages treat client as an implicit member - // of the group whereas this message explicitly included them so to - // be consistent for now we will remove the client from the - // received hashset - if let Some(uid) = self.uid() { - self.group_members.remove(&uid); - } - }, - NoGroup => { - self.group_leader = None; - self.group_members = HashSet::new(); + use comp::group::ChangeNotification::*; + // Note: we use a hashmap since this would not work with entities outside + // the view distance + match change_notification { + Added(uid, role) => { + if self.group_members.insert(uid, role) == Some(role) { + warn!( + "Received msg to add uid {} to the group members but they \ + were already there", + uid + ); } - } + }, + Removed(uid) => { + if self.group_members.remove(&uid).is_none() { + warn!( + "Received msg to remove uid {} from group members but by they \ + weren't in there!", + uid + ); + } + }, + NewLeader(leader) => { + self.group_leader = Some(leader); + }, + NewGroup { leader, members } => { + self.group_leader = Some(leader); + self.group_members = members.into_iter().collect(); + // Currently add/remove messages treat client as an implicit member + // of the group whereas this message explicitly included them so to + // be consistent for now we will remove the client from the + // received hashset + if let Some(uid) = self.uid() { + self.group_members.remove(&uid); + } + }, + NoGroup => { + self.group_leader = None; + self.group_members = HashMap::new(); + }, + } }, ServerMsg::GroupInvite(uid) => { self.group_invite = Some(uid); @@ -1189,9 +1198,7 @@ impl Client { pub fn entity(&self) -> EcsEntity { self.entity } /// Get the player's Uid. - pub fn uid(&self) -> Option { - self.state.read_component_copied(self.entity) - } + pub fn uid(&self) -> Option { self.state.read_component_copied(self.entity) } /// Get the client state pub fn get_client_state(&self) -> ClientState { self.client_state } diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 517a4e705d..3c2c6c8828 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -22,7 +22,7 @@ pub enum InventoryManip { pub enum GroupManip { Invite(Uid), Accept, - Reject, + Decline, Leave, Kick(Uid), AssignLeader(Uid), diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index acbcf17fa1..2fcc570467 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -11,7 +11,6 @@ use tracing::{error, warn}; // - no support for more complex group structures // - lack of complex enemy npc integration // - relies on careful management of groups to maintain a valid state -// - clients don't know what entities are their pets // - the possesion rod could probably wreck this #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -35,15 +34,21 @@ pub struct GroupInfo { pub name: String, } +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum Role { + Member, + Pet, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChangeNotification { // :D - Added(E), + Added(E, Role), // :( Removed(E), NewLeader(E), // Use to put in a group overwriting existing group - NewGroup { leader: E, members: Vec }, + NewGroup { leader: E, members: Vec<(E, Role)> }, // No longer in a group NoGroup, } @@ -54,14 +59,17 @@ pub enum ChangeNotification { impl ChangeNotification { pub fn try_map(self, f: impl Fn(E) -> Option) -> Option> { match self { - Self::Added(e) => f(e).map(ChangeNotification::Added), + Self::Added(e, r) => f(e).map(|t| ChangeNotification::Added(t, r)), Self::Removed(e) => f(e).map(ChangeNotification::Removed), Self::NewLeader(e) => f(e).map(ChangeNotification::NewLeader), // Note just discards members that fail map Self::NewGroup { leader, members } => { f(leader).map(|leader| ChangeNotification::NewGroup { leader, - members: members.into_iter().filter_map(f).collect(), + members: members + .into_iter() + .filter_map(|(e, r)| f(e).map(|t| (t, r))) + .collect(), }) }, Self::NoGroup => Some(ChangeNotification::NoGroup), @@ -79,23 +87,21 @@ pub struct GroupManager { groups: Slab, } -// Gather list of pets of the group member + member themselves +// Gather list of pets of the group member // Note: iterating through all entities here could become slow at higher entity // counts -fn with_pets( +fn pets( entity: specs::Entity, uid: Uid, alignments: &Alignments, entities: &specs::Entities, ) -> Vec { - let mut list = (entities, alignments) + (entities, alignments) .join() .filter_map(|(e, a)| { matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e) }) - .collect::>(); - list.push(entity); - list + .collect::>() } /// Returns list of current members of a group @@ -103,10 +109,23 @@ pub fn members<'a>( group: Group, groups: impl Join + 'a, entities: &'a specs::Entities, -) -> impl Iterator + 'a { - (entities, groups) + alignments: &'a Alignments, + uids: &'a Uids, +) -> impl Iterator + 'a { + (entities, groups, alignments, uids) .join() - .filter_map(move |(e, g)| (*g == group).then_some(e)) + .filter_map(move |(e, g, a, u)| { + (*g == group).then(|| { + ( + e, + if matches!(a, Alignment::Owned(owner) if owner != u) { + Role::Pet + } else { + Role::Member + }, + ) + }) + }) } // TODO: optimize add/remove for massive NPC groups @@ -194,23 +213,30 @@ impl GroupManager { new_group }); - let member_plus_pets = with_pets(new_member, new_member_uid, alignments, entities); + let new_pets = pets(new_member, new_member_uid, alignments, entities); // Inform - members(group, &*groups, entities).for_each(|a| { - member_plus_pets.iter().for_each(|b| { - notifier(a, ChangeNotification::Added(*b)); - notifier(*b, ChangeNotification::Added(a)); - }) + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { + Role::Member => { + notifier(e, ChangeNotification::Added(new_member, Role::Member)); + notifier(new_member, ChangeNotification::Added(e, Role::Member)); + + new_pets.iter().for_each(|p| { + notifier(e, ChangeNotification::Added(*p, Role::Pet)); + }) + }, + Role::Pet => { + notifier(new_member, ChangeNotification::Added(e, Role::Pet)); + }, }); - // Note: pets not informed notifier(new_member, ChangeNotification::NewLeader(leader)); // Add group id for new member and pets // Unwrap should not fail since we just found these entities and they should // still exist // Note: if there is an issue replace with a warn - member_plus_pets.iter().for_each(|e| { + let _ = groups.insert(new_member, group).unwrap(); + new_pets.iter().for_each(|e| { let _ = groups.insert(*e, group).unwrap(); }); } @@ -221,6 +247,8 @@ impl GroupManager { owner: specs::Entity, groups: &mut GroupsMut, entities: &specs::Entities, + alignments: &Alignments, + uids: &Uids, notifier: &mut impl FnMut(specs::Entity, ChangeNotification), ) { let group = match groups.get(owner).copied() { @@ -235,17 +263,15 @@ impl GroupManager { }; // Inform - members(group, &*groups, entities).for_each(|a| { - notifier(a, ChangeNotification::Added(pet)); - notifier(pet, ChangeNotification::Added(a)); + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { + Role::Member => { + notifier(e, ChangeNotification::Added(pet, Role::Pet)); + }, + Role::Pet => {}, }); // Add groups.insert(pet, group).unwrap(); - - if let Some(info) = self.group_info(group) { - notifier(pet, ChangeNotification::NewLeader(info.leader)); - } } pub fn leave_group( @@ -341,32 +367,30 @@ impl GroupManager { .for_each(|(owner, pets)| { if let Some(owner) = owner { if !pets.is_empty() { - let mut members = pets.clone(); - members.push(owner); + let mut members = + pets.iter().map(|e| (*e, Role::Pet)).collect::>(); + members.push((owner, Role::Member)); // New group let new_group = self.create_group(owner); - for &member in &members { - groups.insert(member, new_group).unwrap(); + for (member, _) in &members { + groups.insert(*member, new_group).unwrap(); } - let notification = ChangeNotification::NewGroup { + notifier(owner, ChangeNotification::NewGroup { leader: owner, members, - }; - - // TODO: don't clone - notifier(owner, notification.clone()); - pets.into_iter() - .for_each(|pet| notifier(pet, notification.clone())); + }); } else { // If no pets just remove group groups.remove(owner); notifier(owner, ChangeNotification::NoGroup) } } else { - pets.into_iter() - .for_each(|pet| notifier(pet, ChangeNotification::NoGroup)); + // Owner not found, potentially the were removed from the world + pets.into_iter().for_each(|pet| { + groups.remove(pet); + }); } }); } else { @@ -378,36 +402,47 @@ impl GroupManager { return; }; - let leaving = with_pets(member, leaving_member_uid, alignments, entities); + let leaving_pets = pets(member, leaving_member_uid, alignments, entities); // If pets and not about to be deleted form new group - if leaving.len() > 1 && !to_be_deleted { + if !leaving_pets.is_empty() && !to_be_deleted { let new_group = self.create_group(member); - let notification = ChangeNotification::NewGroup { + notifier(member, ChangeNotification::NewGroup { leader: member, - members: leaving.clone(), - }; + members: leaving_pets + .iter() + .map(|p| (*p, Role::Pet)) + .chain(std::iter::once((member, Role::Member))) + .collect(), + }); - leaving.iter().for_each(|&e| { + let _ = groups.insert(member, new_group).unwrap(); + leaving_pets.iter().for_each(|&e| { let _ = groups.insert(e, new_group).unwrap(); - notifier(e, notification.clone()); }); } else { - leaving.iter().for_each(|&e| { + let _ = groups.remove(member); + notifier(member, ChangeNotification::NoGroup); + leaving_pets.iter().for_each(|&e| { let _ = groups.remove(e); - notifier(e, ChangeNotification::NoGroup); }); } if let Some(info) = self.group_info(group) { // Inform remaining members let mut num_members = 0; - members(group, &*groups, entities).for_each(|a| { + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| { num_members += 1; - leaving.iter().for_each(|b| { - notifier(a, ChangeNotification::Removed(*b)); - }) + match role { + Role::Member => { + notifier(e, ChangeNotification::Removed(member)); + leaving_pets.iter().for_each(|p| { + notifier(e, ChangeNotification::Removed(*p)); + }) + }, + Role::Pet => {}, + } }); // If leader is the last one left then disband the group // Assumes last member is the leader @@ -430,6 +465,8 @@ impl GroupManager { new_leader: specs::Entity, groups: &Groups, entities: &specs::Entities, + alignments: &Alignments, + uids: &Uids, mut notifier: impl FnMut(specs::Entity, ChangeNotification), ) { let group = match groups.get(new_leader) { @@ -441,8 +478,9 @@ impl GroupManager { self.groups[group.0 as usize].leader = new_leader; // Point to new leader - members(group, groups, entities).for_each(|e| { - notifier(e, ChangeNotification::NewLeader(new_leader)); + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role { + Role::Member => notifier(e, ChangeNotification::NewLeader(new_leader)), + Role::Pet => {}, }); } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index eb14d24e24..3c5601f4e0 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -569,6 +569,8 @@ fn handle_spawn( target, &mut state.ecs().write_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, &mut |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index bfc15bb946..e73a617fdc 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -136,7 +136,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ); } }, - GroupManip::Reject => { + GroupManip::Decline => { let mut clients = state.ecs().write_storage::(); if let Some(inviter) = clients .get_mut(entity) @@ -144,8 +144,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani { // Inform inviter of rejection if let Some(client) = clients.get_mut(inviter) { - // TODO: say who rejected the invite - client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + // TODO: say who declined the invite + client.notify(ChatType::Meta.server_msg("Invite declined".to_owned())); } } }, @@ -296,6 +296,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani target, &groups, &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index e1d658fb04..549804d6f8 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -236,6 +236,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv entity, &mut state.ecs().write_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, &mut |entity, group_change| { clients .get_mut(entity) diff --git a/server/src/events/player.rs b/server/src/events/player.rs index 1e777c1daf..c81482c199 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -57,6 +57,8 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { new_entity, &state.ecs().read_storage(), &state.ecs().entities(), + &state.ecs().read_storage(), + &state.ecs().read_storage(), // Nothing actually changing |_, _| {}, ); diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 759c469004..0528e8cfba 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -5,8 +5,8 @@ use crate::{ }; use common::{ comp::{ - Admin, AdminList, CanBuild, ChatMode, UnresolvedChatMsg, ChatType, ControlEvent, Controller, - ForceUpdate, Ori, Player, Pos, Stats, Vel, + Admin, AdminList, CanBuild, ChatMode, ChatType, ControlEvent, Controller, ForceUpdate, Ori, + Player, Pos, Stats, UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, msg::{ diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 24b43e360b..5a6124dcac 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,13 +1,16 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, TEXT_COLOR_GREY, UI_MAIN}; -use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use crate::{ + i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, window::GameInput, +}; use client::{self, Client}; use common::{ - comp::Stats, + comp::{group::Role, Stats}, sync::{Uid, WorldSyncExt}, }; use conrod_core::{ color, + position::{Place, Relative}, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; @@ -25,60 +28,51 @@ widget_ids! { btn_link, btn_kick, btn_leave, + scroll_area, + scrollbar, members[], invite_bubble, bubble_frame, btn_accept, btn_decline, - // TEST - test_leader, - test_member1, - test_member2, - test_member3, - test_member4, - test_member5, } } pub struct State { ids: Ids, - // Holds the time when selection is made since this selection can be overriden - // by selecting an entity in-game - selected_uid: Option<(Uid, Instant)>, // Selected group member selected_member: Option, } #[derive(WidgetCommon)] pub struct Group<'a> { - show: &'a Show, + show: &'a mut Show, client: &'a Client, + settings: &'a Settings, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - selected_entity: Option<(specs::Entity, Instant)>, - #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Group<'a> { pub fn new( - show: &'a Show, + show: &'a mut Show, client: &'a Client, + settings: &'a Settings, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { show, client, + settings, imgs, fonts, localized_strings, - selected_entity, common: widget::CommonBuilder::default(), } } @@ -87,7 +81,7 @@ impl<'a> Group<'a> { pub enum Event { Close, Accept, - Reject, + Decline, Kick(Uid), LeaveGroup, AssignLeader(Uid), @@ -101,7 +95,6 @@ impl<'a> Widget for Group<'a> { fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Self::State { ids: Ids::new(id_gen), - selected_uid: None, selected_member: None, } } @@ -114,23 +107,63 @@ impl<'a> Widget for Group<'a> { let mut events = Vec::new(); - let player_leader = true; - let in_group = true; - let open_invite = false; + // Don't show pets + let group_members = self + .client + .group_members() + .iter() + .filter_map(|(u, r)| match r { + Role::Member => Some(u), + Role::Pet => None, + }) + .collect::>(); - if in_group || open_invite { + // Not considered in group for ui purposes if it is just pets + let in_group = !group_members.is_empty(); + + // Helper + let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { + Some(player_info) => player_info + .character + .as_ref() + .map_or_else(|| format!("Player<{}>", uid), |c| c.name.clone()), + None => client + .state() + .ecs() + .entity_from_uid(uid.0) + .and_then(|entity| { + client + .state() + .ecs() + .read_storage::() + .get(entity) + .map(|stats| stats.name.clone()) + }) + .unwrap_or_else(|| format!("Npc<{}>", uid)), + }; + + let open_invite = self.client.group_invite(); + + let my_uid = self.client.uid(); + + // TODO show something to the player when they click on the group button while + // they are not in a group so that it doesn't look like the button is + // broken + + if in_group || open_invite.is_some() { // Frame Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) .bottom_left_with_margins_on(ui.window, 220.0, 10.0) .set(state.ids.bg, ui); - if open_invite { - // yellow animated border - } + if open_invite.is_some() { + // yellow animated border + } } // Buttons - if in_group { - Text::new("Group Name") + if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) { + let selected = state.selected_member; + Text::new(&group_name) .mid_top_with_margin_on(state.ids.bg, 2.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) @@ -144,7 +177,7 @@ impl<'a> Widget for Group<'a> { .label("Add to Friends") .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) + .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_friend, ui) .was_clicked() {}; @@ -153,176 +186,209 @@ impl<'a> Widget for Group<'a> { .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Leave Group") - .label_color(TEXT_COLOR) + .label(&self.localized_strings.get("hud.group.leave")) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) + .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_leave, ui) .was_clicked() - {}; + { + events.push(Event::LeaveGroup); + }; // Group leader functions - if player_leader { - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label("Assign Leader") - .label_color(TEXT_COLOR) // Grey when no player is selected - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_leader, ui) - .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("Link Group") - .label_color(TEXT_COLOR_GREY) // Change this when the linking is working - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_link, ui) - .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) - .down_from(state.ids.btn_link, 5.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label("Kick") - .label_color(TEXT_COLOR) // Grey when no player is selected - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_kick, ui) - .was_clicked() - {}; - } - // Group Members, only character names, cut long names when they exceed the button size - // TODO Insert loop here - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) + if my_uid == Some(leader) { + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.assign_leader")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leader, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::AssignLeader(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Link Group") // TODO: Localize + .label_color(TEXT_COLOR_GREY) // Change this when the linking is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_link, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) + .down_from(state.ids.btn_link, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.kick")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_kick, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Kick(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + } + // Group Members, only character names, cut long names when they exceed the + // button size + let group_size = group_members.len() + 1; + if state.ids.members.len() < group_size { + state.update(|s| { + s.ids + .members + .resize(group_size, &mut ui.widget_id_generator()) + }) + } + // Scrollable area for group member names + Rectangle::fill_with([110.0, 192.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Leader") // Grey when no player is selected - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_leader, ui) - .was_clicked() + .scroll_kids() + .scroll_kids_vertically() + .set(state.ids.scroll_area, ui); + Scrollbar::y_axis(state.ids.scroll_area) + .thickness(5.0) + .rgba(0.33, 0.33, 0.33, 1.0) + .set(state.ids.scrollbar, ui); + // List member names + for (i, &uid) in self + .client + .uid() + .iter() + .chain(group_members.iter().copied()) + .enumerate() { - //Select the Leader - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_leader, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member1, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member1, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member2, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member2, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member3, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member3, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member4, ui) - .was_clicked() - { - // Select the group member - }; - if Button::image(self.imgs.nothing) // if selected self.imgs.selection - .w_h(90.0, 22.0) - .down_from(state.ids.test_member4, 10.0) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .label("Other Player") - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.test_member5, ui) - .was_clicked() - { - // Select the group member - }; - // Maximum of 6 Players/Npcs per Group - // Player pets count as group members, too. They are not counted into the maximum group size. + let selected = state.selected_member.map_or(false, |u| u == uid); + let char_name = uid_to_name_text(uid, &self.client); + // TODO: Do something special visually if uid == leader + if Button::image(if selected { + self.imgs.selection + } else { + self.imgs.nothing + }) + .w_h(100.0, 22.0) + .and(|w| { + if i == 0 { + w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0) + } else { + w.down_from(state.ids.members[i - 1], 10.0) + } + }) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .crop_kids() + .label_x(Relative::Place(Place::Start(Some(4.0)))) + .label(&char_name) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.members[i], ui) + .was_clicked() + { + // Do nothing when clicking yourself + if Some(uid) != my_uid { + // Select the group member + state.update(|s| { + s.selected_member = if selected { None } else { Some(uid) } + }); + } + }; + } + // Maximum of 6 Players/Npcs per Group + // Player pets count as group members, too. They are not counted + // into the maximum group size. } - if open_invite { - //self.show.group = true; Auto open group menu - Text::new("Player wants to invite you!") + if let Some(invite_uid) = open_invite { + self.show.group = true; // Auto open group menu + // TODO: add group name here too + // Invite text + let name = uid_to_name_text(invite_uid, &self.client); + let invite_text = self + .localized_strings + .get("hud.group.invite_to_join") + .replace("{name}", &name); + Text::new(&invite_text) .mid_top_with_margin_on(state.ids.bg, 20.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.title, ui); + // Accept Button + let accept_key = self + .settings + .controls + .get_binding(GameInput::AcceptGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); if Button::image(self.imgs.button) .w_h(90.0, 22.0) .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("[U] Accept") - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&format!( + "[{}] {}", + &accept_key, + &self.localized_strings.get("common.accept") + )) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) - .set(state.ids.btn_friend, ui) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_accept, ui) .was_clicked() - {}; + { + events.push(Event::Accept); + }; + // Decline button + let decline_key = self + .settings + .controls + .get_binding(GameInput::DeclineGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); if Button::image(self.imgs.button) .w_h(90.0, 22.0) .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("[I] Decline") - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&format!( + "[{}] {}", + &decline_key, + &self.localized_strings.get("common.decline") + )) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) - .set(state.ids.btn_friend, ui) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_decline, ui) .was_clicked() - {}; - + { + events.push(Event::Decline); + }; } events } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 773e4897e9..ad6bd8689c 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -310,7 +310,7 @@ pub enum Event { CraftRecipe(String), InviteMember(common::sync::Uid), AcceptInvite, - RejectInvite, + DeclineInvite, KickMember(common::sync::Uid), LeaveGroup, AssignLeader(common::sync::Uid), @@ -1083,7 +1083,7 @@ impl Hud { h, i, uid, - client.group_members.contains(uid), + client.group_members().contains_key(uid), ) }) .filter(|(entity, pos, _, stats, _, _, _, _, hpfl, _, in_group)| { @@ -1945,30 +1945,25 @@ impl Hud { self.show.open_social_tab(social_tab) }, social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), - social::Event::Accept => events.push(Event::AcceptInvite), - social::Event::Reject => events.push(Event::RejectInvite), - social::Event::Kick(uid) => events.push(Event::KickMember(uid)), - social::Event::LeaveGroup => events.push(Event::LeaveGroup), - social::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), } } } // Group Window if self.show.group { for event in Group::new( - &self.show, + &mut self.show, client, + &global_state.settings, &self.imgs, &self.fonts, &self.voxygen_i18n, - info.selected_entity, ) .set(self.ids.group_window, ui_widgets) { match event { group::Event::Close => self.show.social(false), group::Event::Accept => events.push(Event::AcceptInvite), - group::Event::Reject => events.push(Event::RejectInvite), + 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)), diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index a13532f2a0..b42c347398 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -2,16 +2,12 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; -use common::{ - comp::Stats, - sync::{Uid, WorldSyncExt}, -}; +use common::sync::Uid; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use specs::WorldExt; use std::time::Instant; widget_ids! { @@ -31,15 +27,7 @@ widget_ids! { friends_test, faction_test, player_names[], - group, - group_invite, - member_names[], - accept_invite_button, - reject_invite_button, invite_button, - kick_button, - assign_leader_button, - leave_button, } } @@ -48,8 +36,6 @@ pub struct State { // Holds the time when selection is made since this selection can be overriden // by selecting an entity in-game selected_uid: Option<(Uid, Instant)>, - // Selected group member - selected_member: Option, } pub enum SocialTab { @@ -95,13 +81,8 @@ impl<'a> Social<'a> { pub enum Event { Close, - ChangeSocialTab(SocialTab), Invite(Uid), - Accept, - Reject, - Kick(Uid), - LeaveGroup, - AssignLeader(Uid), + ChangeSocialTab(SocialTab), } impl<'a> Widget for Social<'a> { @@ -113,7 +94,6 @@ impl<'a> Widget for Social<'a> { Self::State { ids: Ids::new(id_gen), selected_uid: None, - selected_member: None, } } @@ -270,86 +250,11 @@ impl<'a> Widget for Social<'a> { } } - Text::new(&self.localized_strings.get("hud.group")) - .down(10.0) - .font_size(self.fonts.cyri.scale(20)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.group, ui); - - // Helper - let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { - Some(player_info) => { - let alias = &player_info.player_alias; - let character_name_level = match &player_info.character { - Some(character) => format!("{} Lvl {}", &character.name, &character.level), - None => "".to_string(), // character select or spectating - }; - format!("[{}] {}", alias, character_name_level) - }, - None => self - .client - .state() - .ecs() - .entity_from_uid(uid.0) - .and_then(|entity| { - self.client - .state() - .ecs() - .read_storage::() - .get(entity) - .map(|stats| stats.name.clone()) - }) - .unwrap_or_else(|| format!("NPC Uid: {}", uid)), - }; - - // Accept/Reject Invite - if let Some(invite_uid) = self.client.group_invite() { - let name = uid_to_name_text(invite_uid, &self.client); - let text = self - .localized_strings - .get("hud.group.invite_to_join") - .replace("{name}", &name); - Text::new(&text) - .down(10.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.group_invite, ui); - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("common.accept")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.accept_invite_button, ui) - .was_clicked() - { - events.push(Event::Accept); - } - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("common.reject")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.reject_invite_button, ui) - .was_clicked() - { - events.push(Event::Reject); - } - } else if self // Invite Button + // Invite Button + if self .client - .group_leader() - .map_or(true, |l_uid| self.client.uid() == Some(l_uid)) + .group_info() + .map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid)) { let selected = state.selected_uid.map(|s| s.0).or_else(|| { self.selected_entity @@ -381,129 +286,6 @@ impl<'a> Widget for Social<'a> { } } } - - // Show group members - if let Some(leader) = self.client.group_leader() { - let group_size = self.client.group_members.len() + 1; - if state.ids.member_names.len() < group_size { - state.update(|s| { - s.ids - .member_names - .resize(group_size, &mut ui.widget_id_generator()) - }) - } - // List member names - for (i, &uid) in self - .client - .uid() - .iter() - .chain(self.client.group_members.iter()) - .enumerate() - { - let selected = state.selected_member.map_or(false, |u| u == uid); - let text = uid_to_name_text(uid, &self.client); - let text = if selected { - format!("-> {}", &text) - } else { - text - }; - let text = if uid == leader { - format!("{} (Leader)", &text) - } else { - text - }; - Text::new(&text) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.member_names[i], ui); - // Check for click - if ui - .widget_input(state.ids.member_names[i]) - .clicks() - .left() - .next() - .is_some() - { - state.update(|s| { - s.selected_member = if selected { None } else { Some(uid) } - }); - } - } - - // Show more buttons if leader - if self.client.uid() == Some(leader) { - let selected = state.selected_member; - // Kick - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.kick")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.kick_button, ui) - .was_clicked() - { - if let Some(uid) = selected { - events.push(Event::Kick(uid)); - state.update(|s| { - s.selected_member = None; - }); - } - } - // Assign leader - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.assign_leader")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.assign_leader_button, ui) - .was_clicked() - { - if let Some(uid) = selected { - events.push(Event::AssignLeader(uid)); - state.update(|s| { - s.selected_member = None; - }); - } - } - } - - // Leave group button - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.leave")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.leave_button, ui) - .was_clicked() - { - events.push(Event::LeaveGroup); - } - } } // Friends Tab diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index b37af8c2ee..79d0c46975 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -957,8 +957,8 @@ impl PlayState for SessionState { HudEvent::AcceptInvite => { self.client.borrow_mut().accept_group_invite(); }, - HudEvent::RejectInvite => { - self.client.borrow_mut().reject_group_invite(); + HudEvent::DeclineInvite => { + self.client.borrow_mut().decline_group_invite(); }, HudEvent::KickMember(uid) => { self.client.borrow_mut().kick_from_group(uid); diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index d89833dd23..441108bdb7 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -169,7 +169,9 @@ impl ControlSettings { GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9), GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q), GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt), - GameInput::Select => KeyMouse::Key(VirtualKeyCode::I), + GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y), + GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U), + GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I), } } } @@ -236,6 +238,8 @@ impl Default for ControlSettings { GameInput::Slot10, GameInput::SwapLoadout, GameInput::Select, + GameInput::AcceptGroupInvite, + GameInput::DeclineGroupInvite, ]; for game_input in game_inputs { new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index d338d2ca10..b8bf69c08b 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -68,6 +68,8 @@ pub enum GameInput { AutoWalk, CycleCamera, Select, + AcceptGroupInvite, + DeclineGroupInvite, } impl GameInput { @@ -125,6 +127,8 @@ impl GameInput { GameInput::Slot10 => "gameinput.slot10", GameInput::SwapLoadout => "gameinput.swaploadout", GameInput::Select => "gameinput.select", + GameInput::AcceptGroupInvite => "gameinput.acceptgroupinvite", + GameInput::DeclineGroupInvite => "gameinput.declinegroupinvite", } } From f2ed7efcedc230bc702715968d43fcdee647cf87 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Sun, 26 Jul 2020 01:21:15 +0200 Subject: [PATCH 13/71] group button and menu logic changes group member panels health bar text text shadows and scaling cleanup, don't show the player panel social window assets --- assets/voxygen/element/frames/enemybar_1.png | 3 + .../voxygen/element/frames/enemybar_bg_1.png | 3 + .../element/frames/group_member_bg.png | 3 + .../element/frames/group_member_frame.png | 3 + assets/voxygen/element/misc_bg/social_bg.png | 3 + .../voxygen/element/misc_bg/social_frame.png | 3 + .../element/misc_bg/social_tab_active.png | 3 + .../element/misc_bg/social_tab_inactive.png | 3 + assets/voxygen/i18n/en.ron | 2 + client/src/lib.rs | 2 +- voxygen/src/hud/buttons.rs | 20 - voxygen/src/hud/group.rs | 610 ++++++++++++++---- voxygen/src/hud/img_ids.rs | 2 + voxygen/src/hud/mod.rs | 51 +- voxygen/src/hud/skillbar.rs | 13 +- voxygen/src/hud/social.rs | 4 +- 16 files changed, 529 insertions(+), 199 deletions(-) create mode 100644 assets/voxygen/element/frames/enemybar_1.png create mode 100644 assets/voxygen/element/frames/enemybar_bg_1.png create mode 100644 assets/voxygen/element/frames/group_member_bg.png create mode 100644 assets/voxygen/element/frames/group_member_frame.png create mode 100644 assets/voxygen/element/misc_bg/social_bg.png create mode 100644 assets/voxygen/element/misc_bg/social_frame.png create mode 100644 assets/voxygen/element/misc_bg/social_tab_active.png create mode 100644 assets/voxygen/element/misc_bg/social_tab_inactive.png diff --git a/assets/voxygen/element/frames/enemybar_1.png b/assets/voxygen/element/frames/enemybar_1.png new file mode 100644 index 0000000000..37573662b8 --- /dev/null +++ b/assets/voxygen/element/frames/enemybar_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4cfb11db560dbda70ccf97b99905f01c21839f0ef2000a9d5427c7cc7d741f2 +size 257 diff --git a/assets/voxygen/element/frames/enemybar_bg_1.png b/assets/voxygen/element/frames/enemybar_bg_1.png new file mode 100644 index 0000000000..52266b3c1b --- /dev/null +++ b/assets/voxygen/element/frames/enemybar_bg_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71446655d69af498d4d32855b930150a530854ab56d9c1b64bf66c0340b4aab3 +size 183 diff --git a/assets/voxygen/element/frames/group_member_bg.png b/assets/voxygen/element/frames/group_member_bg.png new file mode 100644 index 0000000000..14c5f9b112 --- /dev/null +++ b/assets/voxygen/element/frames/group_member_bg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d9decab2383d59a28e70fe2a932687a861516d6a374f745c849dc6d9f456b41 +size 331 diff --git a/assets/voxygen/element/frames/group_member_frame.png b/assets/voxygen/element/frames/group_member_frame.png new file mode 100644 index 0000000000..f0043a0f5f --- /dev/null +++ b/assets/voxygen/element/frames/group_member_frame.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3effc4f33ea986ae8de70beb0c13b3f85d0c488125a8be116a9848d67b348cb4 +size 329 diff --git a/assets/voxygen/element/misc_bg/social_bg.png b/assets/voxygen/element/misc_bg/social_bg.png new file mode 100644 index 0000000000..6e57026acb --- /dev/null +++ b/assets/voxygen/element/misc_bg/social_bg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ee5e1459b78ef37b9947250401c8732a3011e6105c379260dc99feefcbfcef9 +size 6369 diff --git a/assets/voxygen/element/misc_bg/social_frame.png b/assets/voxygen/element/misc_bg/social_frame.png new file mode 100644 index 0000000000..f0216d61f0 --- /dev/null +++ b/assets/voxygen/element/misc_bg/social_frame.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22ca239ce2b26552546465f85bc2c3c8499553cc06cfaff897f88b3a01e2af2e +size 5880 diff --git a/assets/voxygen/element/misc_bg/social_tab_active.png b/assets/voxygen/element/misc_bg/social_tab_active.png new file mode 100644 index 0000000000..854d4e224a --- /dev/null +++ b/assets/voxygen/element/misc_bg/social_tab_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a641284ee155e1c9e80fc05d214303195de4469b1ad372127db89244aa2e82b +size 470 diff --git a/assets/voxygen/element/misc_bg/social_tab_inactive.png b/assets/voxygen/element/misc_bg/social_tab_inactive.png new file mode 100644 index 0000000000..c974c14514 --- /dev/null +++ b/assets/voxygen/element/misc_bg/social_tab_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46fbea0721ccf1fea5963c2745b049fe441c1f25049af2882f10bea881fa67f1 +size 477 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 1a067a3229..bc70ccabb6 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -326,6 +326,8 @@ magically infused items?"#, "hud.group.kick": "Kick", "hud.group.assign_leader": "Assign Leader", "hud.group.leave": "Leave Group", + "hud.group.dead" : "Dead", + "hud.group.out_of_range": "Out of range", "hud.spell": "Spells", diff --git a/client/src/lib.rs b/client/src/lib.rs index 6c861c7ee2..1f459ed113 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -34,7 +34,7 @@ use common::{ use futures_executor::block_on; use futures_timer::Delay; use futures_util::{select, FutureExt}; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use image::DynamicImage; use network::{ Network, Participant, Pid, ProtocolAddr, Stream, PROMISES_CONSISTENCY, PROMISES_ORDERED, diff --git a/voxygen/src/hud/buttons.rs b/voxygen/src/hud/buttons.rs index 499242ef1b..2c57697873 100644 --- a/voxygen/src/hud/buttons.rs +++ b/voxygen/src/hud/buttons.rs @@ -98,7 +98,6 @@ pub enum Event { ToggleSocial, ToggleSpell, ToggleCrafting, - ToggleGroup, } impl<'a> Widget for Buttons<'a> { @@ -399,25 +398,6 @@ impl<'a> Widget for Buttons<'a> { .set(state.ids.crafting_text, ui); } - // Group - if Button::image(self.imgs.group_icon) - .w_h(49.0, 26.0) - .bottom_left_with_margins_on(ui.window, 190.0, 10.0) - .hover_image(self.imgs.group_icon_hover) - .press_image(self.imgs.group_icon_press) - .with_tooltip( - self.tooltip_manager, - &localized_strings.get("hud.group"), - "", - &button_tooltip, - ) - .bottom_offset(TOOLTIP_UPSHIFT) - .set(state.ids.group_button, ui) - .was_clicked() - { - return Some(Event::ToggleGroup); - } - None } } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 5a6124dcac..542c67e38f 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,7 +1,15 @@ -use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, TEXT_COLOR_GREY, UI_MAIN}; +use super::{ + img_ids::{Imgs, ImgsRot}, + Show, BLACK, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, + TEXT_COLOR_GREY, TRANSPARENT, UI_HIGHLIGHT_0, +}; use crate::{ - i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, window::GameInput, + i18n::VoxygenLocalization, + settings::Settings, + ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, + window::GameInput, + GlobalState, }; use client::{self, Client}; use common::{ @@ -12,13 +20,13 @@ use conrod_core::{ color, position::{Place, Relative}, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, - widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, + widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use specs::WorldExt; -use std::time::Instant; +use specs::{saveload::MarkerAllocator, WorldExt}; widget_ids! { pub struct Ids { + group_button, bg, title, close, @@ -35,6 +43,14 @@ widget_ids! { bubble_frame, btn_accept, btn_decline, + member_panels_bg[], + member_panels_frame[], + member_panels_txt_bg[], + member_panels_txt[], + member_health[], + member_stam[], + dead_txt[], + health_txt[], } } @@ -44,6 +60,7 @@ pub struct State { selected_member: Option, } +const TOOLTIP_UPSHIFT: f64 = 40.0; #[derive(WidgetCommon)] pub struct Group<'a> { show: &'a mut Show, @@ -52,6 +69,10 @@ pub struct Group<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + tooltip_manager: &'a mut TooltipManager, + rot_imgs: &'a ImgsRot, + pulse: f32, + global_state: &'a GlobalState, #[conrod(common_builder)] common: widget::CommonBuilder, @@ -65,21 +86,28 @@ impl<'a> Group<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + tooltip_manager: &'a mut TooltipManager, + rot_imgs: &'a ImgsRot, + pulse: f32, + global_state: &'a GlobalState, ) -> Self { Self { show, client, settings, imgs, + rot_imgs, + tooltip_manager, fonts, localized_strings, + pulse, + global_state, common: widget::CommonBuilder::default(), } } } pub enum Event { - Close, Accept, Decline, Kick(Uid), @@ -102,10 +130,34 @@ impl<'a> Widget for Group<'a> { #[allow(clippy::unused_unit)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } + //TODO: Disband groups when there's only one member in them + //TODO: Always send health, energy, level and position of group members to the + // client + 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 button_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)) + .title_text_color(TEXT_COLOR) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); // Don't show pets let group_members = self @@ -117,7 +169,6 @@ impl<'a> Widget for Group<'a> { Role::Pet => None, }) .collect::>(); - // Not considered in group for ui purposes if it is just pets let in_group = !group_members.is_empty(); @@ -149,27 +200,295 @@ impl<'a> Widget for Group<'a> { // TODO show something to the player when they click on the group button while // they are not in a group so that it doesn't look like the button is // broken - - if in_group || open_invite.is_some() { + if self.show.group_menu || open_invite.is_some() { // Frame Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) .bottom_left_with_margins_on(ui.window, 220.0, 10.0) .set(state.ids.bg, ui); - if open_invite.is_some() { - // yellow animated border - } } - + if open_invite.is_some() { + // Group Menu button + Button::image(self.imgs.group_icon) + .w_h(49.0, 26.0) + .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .set(state.ids.group_button, ui); + } // Buttons if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) { - let selected = state.selected_member; - Text::new(&group_name) - .mid_top_with_margin_on(state.ids.bg, 2.0) - .font_size(20) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.title, ui); - if Button::image(self.imgs.button) + // Group Menu Button + if Button::image(if self.show.group_menu { + self.imgs.group_icon_press + } else { + self.imgs.group_icon + }) + .w_h(49.0, 26.0) + .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .hover_image(self.imgs.group_icon_hover) + .press_image(self.imgs.group_icon_press) + .with_tooltip( + self.tooltip_manager, + &localized_strings.get("hud.group"), + "", + &button_tooltip, + ) + .bottom_offset(TOOLTIP_UPSHIFT) + .set(state.ids.group_button, ui) + .was_clicked() + { + self.show.group_menu = !self.show.group_menu; + }; + // Member panels + let group_size = group_members.len() + 1; + if state.ids.member_panels_bg.len() < group_size { + state.update(|s| { + s.ids + .member_panels_bg + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.member_health.len() < group_size { + state.update(|s| { + s.ids + .member_health + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.member_stam.len() < group_size { + state.update(|s| { + s.ids + .member_stam + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.member_panels_frame.len() < group_size { + state.update(|s| { + s.ids + .member_panels_frame + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.member_panels_txt.len() < group_size { + state.update(|s| { + s.ids + .member_panels_txt + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.dead_txt.len() < group_size { + state.update(|s| { + s.ids + .dead_txt + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.health_txt.len() < group_size { + state.update(|s| { + s.ids + .health_txt + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + if state.ids.member_panels_txt_bg.len() < group_size { + state.update(|s| { + s.ids + .member_panels_txt_bg + .resize(group_size, &mut ui.widget_id_generator()) + }) + }; + + let client_state = self.client.state(); + let stats = client_state.ecs().read_storage::(); + let energy = client_state.ecs().read_storage::(); + let uid_allocator = client_state + .ecs() + .read_resource::(); + + for (i, &uid) in self + .client + .uid() + .iter() + .chain(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)); + 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 { + 210.0 + } else { + 110.0 + }; + let pos = 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) + }; + let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer + let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); + let health_col = match (health_perc * 100.0) as u8 { + 0..=20 => crit_hp_color, + 21..=40 => LOW_HP_COLOR, + _ => HP_COLOR, + }; + // Don't show panel for the player! + // Panel BG + pos.w_h(152.0, 36.0) + .color(if i == 0 { + Some(TRANSPARENT) + } else { + Some(TEXT_COLOR) + }) + .set(state.ids.member_panels_bg[i], ui); + // Health + Image::new(self.imgs.bar_content) + .w_h(148.0 * health_perc, 22.0) + .color(if i == 0 { + Some(TRANSPARENT) + } else { + Some(health_col) + }) + .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0) + .set(state.ids.member_health[i], ui); + if stats.is_dead { + // Death Text + Text::new(&self.localized_strings.get("hud.group.dead")) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(if i == 0 { TRANSPARENT } else { KILL_COLOR }) + .set(state.ids.dead_txt[i], ui); + } else { + // Health Text + let txt = format!( + "{}/{}", + stats.health.current() as u32, + stats.health.maximum() as u32, + ); + let font_size = match stats.health.maximum() { + 0..=999 => 14, + 1000..=9999 => 13, + 10000..=99999 => 12, + _ => 11, + }; + let txt_offset = match stats.health.maximum() { + 0..=999 => 4.0, + 1000..=9999 => 4.5, + 10000..=99999 => 5.0, + _ => 5.5, + }; + Text::new(&txt) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .color(if i == 0 { + TRANSPARENT + } else { + Color::Rgba(1.0, 1.0, 1.0, 0.5) + }) + .set(state.ids.health_txt[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(if i == 0 { + Some(TRANSPARENT) + } else { + Some(UI_HIGHLIGHT_0) + }) + .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(if i == 0 { TRANSPARENT } else { BLACK }) + .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 i == 0 { TRANSPARENT } else { GROUP_COLOR }) + .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(if i == 0 { + Some(TRANSPARENT) + } else { + Some(MANA_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 { + 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 pos = 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) + }; + pos.w_h(152.0, 36.0) + .color(if i == 0 { + Some(TRANSPARENT) + } else { + 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(if i == 0 { + Some(TRANSPARENT) + } else { + 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(if i == 0 { TRANSPARENT } else { TEXT_COLOR }) + .set(state.ids.dead_txt[i], ui); + } + } + + if self.show.group_menu { + let selected = state.selected_member; + Text::new(&group_name) + .mid_top_with_margin_on(state.ids.bg, 2.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); + if Button::image(self.imgs.button) .w_h(90.0, 22.0) .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) .hover_image(self.imgs.button) @@ -180,47 +499,48 @@ impl<'a> Widget for Group<'a> { .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_friend, ui) .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.leave")) - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_leave, ui) - .was_clicked() - { - events.push(Event::LeaveGroup); - }; - // Group leader functions - if my_uid == Some(leader) { + {}; if Button::image(self.imgs.button) .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.assign_leader")) - .label_color(if state.selected_member.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_GREY - }) + .label(&self.localized_strings.get("hud.group.leave")) + .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_leader, ui) + .set(state.ids.btn_leave, ui) .was_clicked() { - if let Some(uid) = selected { - events.push(Event::AssignLeader(uid)); - state.update(|s| { - s.selected_member = None; - }); - } + self.show.group_menu = false; + events.push(Event::LeaveGroup); }; - if Button::image(self.imgs.button) + // Group leader functions + if my_uid == Some(leader) { + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.assign_leader")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leader, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::AssignLeader(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + if Button::image(self.imgs.button) .w_h(90.0, 22.0) .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) .hover_image(self.imgs.button) @@ -231,105 +551,107 @@ impl<'a> Widget for Group<'a> { .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_link, ui) .was_clicked() - {}; - if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) - .down_from(state.ids.btn_link, 5.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.kick")) - .label_color(if state.selected_member.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_GREY + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) + .down_from(state.ids.btn_link, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.kick")) + .label_color(if state.selected_member.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_GREY + }) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_kick, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Kick(uid)); + state.update(|s| { + s.selected_member = None; + }); + } + }; + } + // Group Members, only character names, cut long names when they exceed the + // button size + let group_size = group_members.len() + 1; + if state.ids.members.len() < group_size { + state.update(|s| { + s.ids + .members + .resize(group_size, &mut ui.widget_id_generator()) }) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_kick, ui) - .was_clicked() + } + // Scrollable area for group member names + Rectangle::fill_with([110.0, 192.0], color::TRANSPARENT) + .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) + .scroll_kids() + .scroll_kids_vertically() + .set(state.ids.scroll_area, ui); + Scrollbar::y_axis(state.ids.scroll_area) + .thickness(5.0) + .rgba(0.33, 0.33, 0.33, 1.0) + .set(state.ids.scrollbar, ui); + // List member names + for (i, &uid) in self + .client + .uid() + .iter() + .chain(group_members.iter().copied()) + .enumerate() { - if let Some(uid) = selected { - events.push(Event::Kick(uid)); - state.update(|s| { - s.selected_member = None; - }); - } - }; - } - // Group Members, only character names, cut long names when they exceed the - // button size - let group_size = group_members.len() + 1; - if state.ids.members.len() < group_size { - state.update(|s| { - s.ids - .members - .resize(group_size, &mut ui.widget_id_generator()) - }) - } - // Scrollable area for group member names - Rectangle::fill_with([110.0, 192.0], color::TRANSPARENT) - .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) - .scroll_kids() - .scroll_kids_vertically() - .set(state.ids.scroll_area, ui); - Scrollbar::y_axis(state.ids.scroll_area) - .thickness(5.0) - .rgba(0.33, 0.33, 0.33, 1.0) - .set(state.ids.scrollbar, ui); - // List member names - for (i, &uid) in self - .client - .uid() - .iter() - .chain(group_members.iter().copied()) - .enumerate() - { - let selected = state.selected_member.map_or(false, |u| u == uid); - let char_name = uid_to_name_text(uid, &self.client); + let selected = state.selected_member.map_or(false, |u| u == uid); + let char_name = uid_to_name_text(uid, &self.client); - // TODO: Do something special visually if uid == leader - if Button::image(if selected { - self.imgs.selection - } else { - self.imgs.nothing - }) - .w_h(100.0, 22.0) - .and(|w| { - if i == 0 { - w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0) + // TODO: Do something special visually if uid == leader + if Button::image(if selected { + self.imgs.selection } else { - w.down_from(state.ids.members[i - 1], 10.0) - } - }) - .hover_image(self.imgs.selection_hover) - .press_image(self.imgs.selection_press) - .crop_kids() - .label_x(Relative::Place(Place::Start(Some(4.0)))) - .label(&char_name) - .label_color(TEXT_COLOR) - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(12)) - .set(state.ids.members[i], ui) - .was_clicked() - { - // Do nothing when clicking yourself - if Some(uid) != my_uid { - // Select the group member - state.update(|s| { - s.selected_member = if selected { None } else { Some(uid) } - }); - } - }; + self.imgs.nothing + }) + .w_h(100.0, 22.0) + .and(|w| { + if i == 0 { + w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0) + } else { + w.down_from(state.ids.members[i - 1], 10.0) + } + }) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .crop_kids() + .label_x(Relative::Place(Place::Start(Some(4.0)))) + .label(&char_name) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.members[i], ui) + .was_clicked() + { + // Do nothing when clicking yourself + if Some(uid) != my_uid { + // Select the group member + state.update(|s| { + s.selected_member = if selected { None } else { Some(uid) } + }); + } + }; + } + // Maximum of 6 Players/Npcs per Group + // Player pets count as group members, too. They are not counted + // into the maximum group size. } - // Maximum of 6 Players/Npcs per Group - // Player pets count as group members, too. They are not counted - // into the maximum group size. } if let Some(invite_uid) = open_invite { self.show.group = true; // Auto open group menu // TODO: add group name here too // Invite text + let name = uid_to_name_text(invite_uid, &self.client); let invite_text = self .localized_strings @@ -337,7 +659,7 @@ impl<'a> Widget for Group<'a> { .replace("{name}", &name); Text::new(&invite_text) .mid_top_with_margin_on(state.ids.bg, 20.0) - .font_size(20) + .font_size(12) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) .set(state.ids.title, ui); @@ -364,6 +686,7 @@ impl<'a> Widget for Group<'a> { .was_clicked() { events.push(Event::Accept); + self.show.group_menu = true; }; // Decline button let decline_key = self @@ -390,6 +713,7 @@ impl<'a> Widget for Group<'a> { events.push(Event::Decline); }; } + events } } diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index afb6c54b3f..4bf4298999 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -76,6 +76,8 @@ image_ids! { // Group Window + member_frame: "voxygen.element.frames.group_member_frame", + member_bg: "voxygen.element.frames.group_member_bg", // Chat-Arrows chat_arrow: "voxygen.element.buttons.arrow_down", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index ad6bd8689c..e09d26ce34 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -82,6 +82,7 @@ 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 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); @@ -370,6 +371,7 @@ pub struct Show { social: bool, spell: bool, group: bool, + group_menu: bool, esc_menu: bool, open_windows: Windows, map: bool, @@ -404,11 +406,6 @@ impl Show { } } - fn group(&mut self, open: bool) { - self.group = open; - self.want_grab = !open; - } - fn social(&mut self, open: bool) { if !self.esc_menu { self.social = open; @@ -437,8 +434,6 @@ impl Show { fn toggle_map(&mut self) { self.map(!self.map) } - fn toggle_group(&mut self) { self.group(!self.group) } - fn toggle_mini_map(&mut self) { self.mini_map = !self.mini_map; } fn settings(&mut self, open: bool) { @@ -624,6 +619,7 @@ impl Hud { social: false, spell: false, group: false, + group_menu: false, mini_map: true, settings_tab: SettingsTab::Interface, social_tab: SocialTab::Online, @@ -1610,7 +1606,6 @@ impl Hud { Some(buttons::Event::ToggleSpell) => self.show.toggle_spell(), Some(buttons::Event::ToggleMap) => self.show.toggle_map(), Some(buttons::Event::ToggleCrafting) => self.show.toggle_crafting(), - Some(buttons::Event::ToggleGroup) => self.show.toggle_group(), None => {}, } } @@ -1949,25 +1944,27 @@ impl Hud { } } // Group Window - if self.show.group { - for event in Group::new( - &mut self.show, - client, - &global_state.settings, - &self.imgs, - &self.fonts, - &self.voxygen_i18n, - ) - .set(self.ids.group_window, ui_widgets) - { - match event { - group::Event::Close => self.show.social(false), - 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)), - } + + for event in Group::new( + &mut self.show, + client, + &global_state.settings, + &self.imgs, + &self.fonts, + &self.voxygen_i18n, + tooltip_manager, + &self.rot_imgs, + 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)), } } diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 6fdb6cdbd5..f365473e1d 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -1153,15 +1153,14 @@ impl<'a> Widget for Skillbar<'a> { .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(if hp_percentage <= 20.0 { - crit_hp_color - } else if hp_percentage <= 40.0 { - LOW_HP_COLOR - } else { - HP_COLOR - })) + .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 diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index b42c347398..49f840fd9e 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -105,8 +105,10 @@ impl<'a> Widget for Social<'a> { let mut events = Vec::new(); + let pos = if self.show.group { 180.0 } else { 25.0 }; + Image::new(self.imgs.window_3) - .top_left_with_margins_on(ui.window, 200.0, 25.0) + .top_left_with_margins_on(ui.window, 200.0, pos) .color(Some(UI_MAIN)) .w_h(103.0 * 4.0, 122.0 * 4.0) .set(state.ids.social_frame, ui); From 03d9992b9fdaaf949623d434424c56c6e6fcd3d2 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sat, 25 Jul 2020 21:04:43 -0400 Subject: [PATCH 14/71] Distribute exp evenly and make it easier to target entities --- server/src/events/entity_manipulation.rs | 4 ++-- voxygen/src/session.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index bbfe1488f1..cdb3ee8ba0 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -63,8 +63,8 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc cause { const MAX_EXP_DIST: f32 = 150.0; - // Attacker gets double exp of everyone else - const ATTACKER_EXP_WEIGHT: f32 = 2.0; + // Attacker gets same as exp of everyone else + const ATTACKER_EXP_WEIGHT: f32 = 1.0; let mut exp_reward = (entity_stats.body_type.base_exp() + entity_stats.level.level() * entity_stats.body_type.base_exp_increase()) as f32; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 79d0c46975..3d2f84f834 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1123,7 +1123,7 @@ fn under_cursor( .join() .filter(|(e, _, _, _)| *e != player_entity) .map(|(e, p, s, b)| { - const RADIUS_SCALE: f32 = 1.6; + const RADIUS_SCALE: f32 = 3.0; let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; // Move position up from the feet let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); From 58df00d80cc3b615b03da16be10b43a959e573f7 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Mon, 27 Jul 2020 05:37:09 +0200 Subject: [PATCH 15/71] Social window assets --- .../element/buttons/social_tab_active.png | 3 + .../element/buttons/social_tab_inactive.png | 3 + assets/voxygen/i18n/en.ron | 6 +- .../dungeon/misc_entrance/tower-ruin.vox | 4 +- voxygen/src/hud/img_ids.rs | 17 +- voxygen/src/hud/mod.rs | 38 +- voxygen/src/hud/social.rs | 433 +++++++++++++++--- 7 files changed, 409 insertions(+), 95 deletions(-) create mode 100644 assets/voxygen/element/buttons/social_tab_active.png create mode 100644 assets/voxygen/element/buttons/social_tab_inactive.png diff --git a/assets/voxygen/element/buttons/social_tab_active.png b/assets/voxygen/element/buttons/social_tab_active.png new file mode 100644 index 0000000000..854d4e224a --- /dev/null +++ b/assets/voxygen/element/buttons/social_tab_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a641284ee155e1c9e80fc05d214303195de4469b1ad372127db89244aa2e82b +size 470 diff --git a/assets/voxygen/element/buttons/social_tab_inactive.png b/assets/voxygen/element/buttons/social_tab_inactive.png new file mode 100644 index 0000000000..c974c14514 --- /dev/null +++ b/assets/voxygen/element/buttons/social_tab_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46fbea0721ccf1fea5963c2745b049fe441c1f25049af2882f10bea881fa67f1 +size 477 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index bc70ccabb6..302d545d7c 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -308,11 +308,15 @@ magically infused items?"#, "hud.settings.reset_keybinds": "Reset to Defaults", "hud.social": "Social", - "hud.social.online": "Online", + "hud.social.online": "Online:", "hud.social.friends": "Friends", "hud.social.not_yet_available": "Not yet available", "hud.social.faction": "Faction", "hud.social.play_online_fmt": "{nb_player} player(s) online", + "hud.social.name": "Name", + "hud.social.level": "Level", + "hud.social.zone": "Zone", + "hud.crafting": "Crafting", "hud.crafting.recipes": "Recipes", diff --git a/assets/world/structure/dungeon/misc_entrance/tower-ruin.vox b/assets/world/structure/dungeon/misc_entrance/tower-ruin.vox index daf8e581ad..ff5a8cbf89 100644 --- a/assets/world/structure/dungeon/misc_entrance/tower-ruin.vox +++ b/assets/world/structure/dungeon/misc_entrance/tower-ruin.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ec122d3d5f63af2210361c215845b582c15e15f9c7f1a9a65afcaf9d77accca -size 53856 +oid sha256:992e17484b853c44497b252edc5c52183e992a640217e851ca3b37477c63ff9a +size 52604 diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 4bf4298999..79eda06010 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -59,14 +59,18 @@ image_ids! { selection_press: "voxygen.element.frames.selection_press", // Social Window - social_button: "voxygen.element.buttons.social_tab", - social_button_pressed: "voxygen.element.buttons.social_tab_pressed", - social_button_hover: "voxygen.element.buttons.social_tab_hover", - social_button_press: "voxygen.element.buttons.social_tab_press", - social_frame: "voxygen.element.frames.social_frame", + social_frame_on: "voxygen.element.misc_bg.social_frame", + social_bg_on: "voxygen.element.misc_bg.social_bg", + social_frame_friends: "voxygen.element.misc_bg.social_frame", + social_bg_friends: "voxygen.element.misc_bg.social_bg", + social_frame_fact: "voxygen.element.misc_bg.social_frame", + social_bg_fact: "voxygen.element.misc_bg.social_bg", + social_tab_act: "voxygen.element.buttons.social_tab_active", + social_tab_inact: "voxygen.element.buttons.social_tab_inactive", + social_tab_inact_hover: "voxygen.element.buttons.social_tab_inactive", + social_tab_inact_press: "voxygen.element.buttons.social_tab_inactive", // Crafting Window - crafting_window: "voxygen.element.misc_bg.crafting", crafting_frame: "voxygen.element.misc_bg.crafting_frame", crafting_icon_bordered: "voxygen.element.icons.anvil", @@ -75,7 +79,6 @@ image_ids! { crafting_icon_press: "voxygen.element.buttons.anvil_press", // Group Window - member_frame: "voxygen.element.frames.group_member_frame", member_bg: "voxygen.element.frames.group_member_bg", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index e09d26ce34..ab485be37c 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1924,22 +1924,28 @@ impl Hud { // Social Window if self.show.social { - for event in Social::new( - &self.show, - client, - &self.imgs, - &self.fonts, - &self.voxygen_i18n, - info.selected_entity, - ) - .set(self.ids.social_window, ui_widgets) - { - match event { - social::Event::Close => self.show.social(false), - social::Event::ChangeSocialTab(social_tab) => { - self.show.open_social_tab(social_tab) - }, - social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), + let ecs = client.state().ecs(); + let stats = ecs.read_storage::(); + let me = client.entity(); + if let Some(stats) = stats.get(me) { + for event in Social::new( + &self.show, + client, + &self.imgs, + &self.fonts, + &self.voxygen_i18n, + &stats, + info.selected_entity, + ) + .set(self.ids.social_window, ui_widgets) + { + match event { + social::Event::Close => self.show.social(false), + social::Event::ChangeSocialTab(social_tab) => { + self.show.open_social_tab(social_tab) + }, + social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), + } } } } diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 49f840fd9e..06e846e5f1 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -1,32 +1,43 @@ -use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN}; +use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; -use common::sync::Uid; +use common::{comp::Stats, sync::Uid}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, - widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, + widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; use std::time::Instant; widget_ids! { pub struct Ids { - social_frame, - social_close, - social_title, frame, - align, - content_align, - online_tab, - friends_tab, - faction_tab, - online_title, - online_no, + close, + title, + bg, + icon, scrollbar, + online_align, + online_tab, + online_tab_icon, + names_align, + name_txt, + player_levels[], + player_names[], + player_zones[], + online_txt, + online_no, + levels_align, + level_txt, + zones_align, + zone_txt, + friends_tab, + //friends_tab_icon, + faction_tab, + //faction_tab_icon, friends_test, faction_test, - player_names[], invite_button, } } @@ -51,6 +62,7 @@ pub struct Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, @@ -65,6 +77,7 @@ impl<'a> Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { @@ -73,6 +86,7 @@ impl<'a> Social<'a> { imgs, fonts, localized_strings, + stats, selected_entity, common: widget::CommonBuilder::default(), } @@ -105,21 +119,41 @@ impl<'a> Widget for Social<'a> { let mut events = Vec::new(); + // Window frame and BG let pos = if self.show.group { 180.0 } else { 25.0 }; - - Image::new(self.imgs.window_3) + // TODO: Different window visuals depending on the selected tab + let window_bg = match &self.show.social_tab { + SocialTab::Online => self.imgs.social_bg_on, + SocialTab::Friends => self.imgs.social_bg_friends, + SocialTab::Faction => self.imgs.social_bg_fact, + }; + let window_frame = match &self.show.social_tab { + SocialTab::Online => self.imgs.social_frame_on, + SocialTab::Friends => self.imgs.social_frame_friends, + SocialTab::Faction => self.imgs.social_frame_fact, + }; + Image::new(window_bg) .top_left_with_margins_on(ui.window, 200.0, pos) .color(Some(UI_MAIN)) - .w_h(103.0 * 4.0, 122.0 * 4.0) - .set(state.ids.social_frame, ui); - + .w_h(280.0, 460.0) + .set(state.ids.bg, ui); + Image::new(window_frame) + .middle_of(state.ids.bg) + .color(Some(UI_HIGHLIGHT_0)) + .w_h(280.0, 460.0) + .set(state.ids.frame, ui); + // Icon + Image::new(self.imgs.social) + .w_h(30.0, 30.0) + .top_left_with_margins_on(state.ids.frame, 6.0, 6.0) + .set(state.ids.icon, ui); // X-Button if Button::image(self.imgs.close_button) - .w_h(28.0, 28.0) + .w_h(24.0, 25.0) .hover_image(self.imgs.close_button_hover) .press_image(self.imgs.close_button_press) - .top_right_with_margins_on(state.ids.social_frame, 0.0, 0.0) - .set(state.ids.social_close, ui) + .top_right_with_margins_on(state.ids.frame, 0.0, 0.0) + .set(state.ids.close, ui) .was_clicked() { events.push(Event::Close); @@ -127,63 +161,324 @@ impl<'a> Widget for Social<'a> { // Title Text::new(&self.localized_strings.get("hud.social")) - .mid_top_with_margin_on(state.ids.social_frame, 6.0) + .mid_top_with_margin_on(state.ids.frame, 9.0) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(14)) + .font_size(self.fonts.cyri.scale(22)) .color(TEXT_COLOR) - .set(state.ids.social_title, ui); + .set(state.ids.title, ui); - // Alignment - Rectangle::fill_with([99.0 * 4.0, 112.0 * 4.0], color::TRANSPARENT) - .mid_top_with_margin_on(state.ids.social_frame, 8.0 * 4.0) - .set(state.ids.align, ui); - // Content Alignment - Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT) - .middle_of(state.ids.frame) - .scroll_kids() - .scroll_kids_vertically() - .set(state.ids.content_align, ui); - Scrollbar::y_axis(state.ids.content_align) - .thickness(5.0) - .rgba(0.33, 0.33, 0.33, 1.0) - .set(state.ids.scrollbar, ui); - // Frame - Image::new(self.imgs.social_frame) - .w_h(99.0 * 4.0, 100.0 * 4.0) - .mid_bottom_of(state.ids.align) - .color(Some(UI_MAIN)) - .set(state.ids.frame, ui); - - // Online Tab - - if Button::image(if let SocialTab::Online = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button + // Tabs Buttons + // Online Tab Button + if Button::image(match &self.show.social_tab { + SocialTab::Online => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact, }) - .w_h(30.0 * 4.0, 12.0 * 4.0) - .hover_image(if let SocialTab::Online = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button_hover + .w_h(30.0, 44.0) + .hover_image(match &self.show.social_tab { + SocialTab::Online => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_hover, }) - .press_image(if let SocialTab::Online = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button_press + .press_image(match &self.show.social_tab { + SocialTab::Online => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_press, }) - .top_left_with_margins_on(state.ids.align, 4.0, 0.0) - .label(&self.localized_strings.get("hud.social.online")) - .label_font_size(self.fonts.cyri.scale(14)) - .label_font_id(self.fonts.cyri.conrod_id) - .parent(state.ids.frame) - .color(UI_MAIN) - .label_color(TEXT_COLOR) + .image_color(match &self.show.social_tab { + SocialTab::Online => UI_MAIN, + _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), + }) + .top_right_with_margins_on(state.ids.frame, 50.0, -28.0) .set(state.ids.online_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Online)); } + Image::new(self.imgs.chat_online_small) + .w_h(20.0, 20.0) + .top_right_with_margins_on(state.ids.online_tab, 12.0, 7.0) + .color(match &self.show.social_tab { + SocialTab::Online => Some(TEXT_COLOR), + _ => Some(UI_MAIN), + }) + .set(state.ids.online_tab_icon, ui); + // Friends Tab Button + if Button::image(match &self.show.social_tab { + SocialTab::Friends => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact, + }) + .w_h(30.0, 44.0) + .hover_image(match &self.show.social_tab { + SocialTab::Friends => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_hover, + }) + .press_image(match &self.show.social_tab { + SocialTab::Friends => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_press, + }) + .down_from(state.ids.online_tab, 0.0) + .image_color(match &self.show.social_tab { + SocialTab::Friends => UI_MAIN, + _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), + }) + .set(state.ids.friends_tab, ui) + .was_clicked() + { + events.push(Event::ChangeSocialTab(SocialTab::Friends)); + } + // Faction Tab Button + if Button::image(match &self.show.social_tab { + SocialTab::Friends => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact, + }) + .w_h(30.0, 44.0) + .hover_image(match &self.show.social_tab { + SocialTab::Faction => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_hover, + }) + .press_image(match &self.show.social_tab { + SocialTab::Faction => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_press, + }) + .down_from(state.ids.friends_tab, 0.0) + .image_color(match &self.show.social_tab { + SocialTab::Faction => UI_MAIN, + _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), + }) + .set(state.ids.faction_tab, ui) + .was_clicked() + { + events.push(Event::ChangeSocialTab(SocialTab::Faction)); + } + // Online Tab + if let SocialTab::Online = self.show.social_tab { + // Content Alignments + Rectangle::fill_with([270.0, 346.0], color::TRANSPARENT) + .mid_top_with_margin_on(state.ids.frame, 74.0) + .scroll_kids_vertically() + .set(state.ids.online_align, ui); + Rectangle::fill_with([133.0, 370.0], color::TRANSPARENT) + .top_left_with_margins_on(state.ids.online_align, 0.0, 0.0) + .crop_kids() + .set(state.ids.names_align, ui); + Rectangle::fill_with([39.0, 370.0], color::TRANSPARENT) + .right_from(state.ids.names_align, 2.0) + .crop_kids() + .set(state.ids.levels_align, ui); + Rectangle::fill_with([94.0, 370.0], color::TRANSPARENT) + .right_from(state.ids.levels_align, 2.0) + .crop_kids() + .set(state.ids.zones_align, ui); + Scrollbar::y_axis(state.ids.online_align) + .thickness(4.0) + .color(UI_HIGHLIGHT_0) + .set(state.ids.scrollbar, ui); + // + // Headlines + // + if Button::image(self.imgs.nothing) + .w_h(133.0, 18.0) + .top_left_with_margins_on(state.ids.frame, 52.0, 7.0) + .label(&self.localized_strings.get("hud.social.name")) + .label_font_size(self.fonts.cyri.scale(14)) + .label_y(conrod_core::position::Relative::Scalar(0.0)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .set(state.ids.name_txt, ui) + .was_clicked() + { + // Sort widgets by name alphabetically + } + if Button::image(self.imgs.nothing) + .w_h(39.0, 18.0) + .right_from(state.ids.name_txt, 2.0) + .label(&self.localized_strings.get("hud.social.level")) + .label_font_size(self.fonts.cyri.scale(14)) + .label_y(conrod_core::position::Relative::Scalar(0.0)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .set(state.ids.level_txt, ui) + .was_clicked() + { + // Sort widgets by level (increasing) + } + if Button::image(self.imgs.nothing) + .w_h(93.0, 18.0) + .right_from(state.ids.level_txt, 2.0) + .label(&self.localized_strings.get("hud.social.zone")) + .label_font_size(self.fonts.cyri.scale(14)) + .label_y(conrod_core::position::Relative::Scalar(0.0)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .set(state.ids.zone_txt, ui) + .was_clicked() + { + // Sort widgets by zone alphabetically + } + // Online Text + let players = self.client.player_list.iter().filter(|(_, p)| p.is_online); + let count = players.clone().count(); + Text::new(&self.localized_strings.get("hud.social.online")) + .bottom_left_with_margins_on(state.ids.frame, 18.0, 10.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .color(TEXT_COLOR) + .set(state.ids.online_txt, ui); + Text::new(&count.to_string()) + .right_from(state.ids.online_txt, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .color(TEXT_COLOR) + .set(state.ids.online_no, ui); + // Adjust widget_id struct vec length to player count + if state.ids.player_levels.len() < count { + state.update(|s| { + s.ids + .player_levels + .resize(count, &mut ui.widget_id_generator()) + }) + }; + if state.ids.player_names.len() < count { + state.update(|s| { + s.ids + .player_names + .resize(count, &mut ui.widget_id_generator()) + }) + }; + if state.ids.player_zones.len() < count { + state.update(|s| { + s.ids + .player_zones + .resize(count, &mut ui.widget_id_generator()) + }) + }; + // Create a name, level and zone row for every player in the list + for (i, (&uid, player_info)) in players.enumerate() { + let selected = state.selected_uid.map_or(false, |u| u.0 == uid); + let alias = &player_info.player_alias; + let name = match &player_info.character { + Some(character) => format!("{} ", &character.name), + None => "".to_string(), // character select or spectating + }; + let level = match &player_info.character { + Some(character) => format!("{} ", &character.level), + None => "".to_string(), // character select or spectating + }; + let setting = true; // TODO Remove this + let zone = "Wilderness"; // TODO: Add real zone + let name_text = if name == self.stats.name { + format!("You ({})", name) // TODO: Locale + } else if setting { + format!("{}", name) + } else { + format!("[{}] {}", alias, name) + }; + // Player name widgets + let button = Button::image(if !selected { + self.imgs.nothing + } else { + self.imgs.selection + }); + let button = if i == 0 { + button.mid_top_with_margin_on(state.ids.names_align, 1.0) + } else { + button.down_from(state.ids.player_names[i - 1], 1.0) + }; + if button + .w_h(133.0, 20.0) + .hover_image(if selected { + self.imgs.selection + } else { + self.imgs.selection_hover + }) + .press_image(if selected { + self.imgs.selection + } else { + self.imgs.selection_press + }) + .label(&name_text) + .label_font_size(self.fonts.cyri.scale(14)) + .label_y(conrod_core::position::Relative::Scalar(0.0)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .set(state.ids.player_names[i], ui) + .was_clicked() + {}; + let level_txt = if i == 0 { + Text::new(&level).mid_top_with_margin_on(state.ids.levels_align, 2.0) + } else { + Text::new(&level).down_from(state.ids.player_levels[i - 1], 2.0) + }; + level_txt + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.player_levels[i], ui); + let zone_txt = if i == 0 { + Text::new(&zone).mid_top_with_margin_on(state.ids.zones_align, 2.0) + } else { + Text::new(&zone).down_from(state.ids.player_zones[i - 1], 2.0) + }; + zone_txt + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.player_zones[i], ui); + + // Check for click + if ui + .widget_input(state.ids.player_names[i]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| s.selected_uid = Some((uid, Instant::now()))); + } + let player = if name == self.stats.name { true } else { false }; + if Button::image(self.imgs.button) + .w_h(106.0, 26.0) + .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0) + .hover_image(if !player && selected { + self.imgs.button_hover + } else { + self.imgs.button + }) + .press_image(if !player && selected { + self.imgs.button_press + } else { + self.imgs.button + }) + .label(&self.localized_strings.get("hud.group.invite")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if !player && selected { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .image_color(if !player && selected { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.invite_button, ui) + .was_clicked() + { + if !player && selected { + events.push(Event::Invite(uid)); + state.update(|s| { + s.selected_uid = None; + }); + } + } + } + // Invite Button + /* */ + } // End of Online Tab + + // Alignment + /* + // Online Tab // Contents @@ -362,7 +657,7 @@ impl<'a> Widget for Social<'a> { .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR_3) .set(state.ids.faction_test, ui); - } + }*/ events } From 1d69e85a50aca7e7af8555504a6711454b8f3982 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 30 Jul 2020 23:46:08 -0400 Subject: [PATCH 16/71] Don't show own char in group menu, handle accept/decline key events --- voxygen/src/hud/group.rs | 83 +++++++++---------------------- voxygen/src/hud/mod.rs | 2 +- voxygen/src/hud/social.rs | 100 +++++++++++++++++++++++--------------- voxygen/src/session.rs | 12 +++++ 4 files changed, 96 insertions(+), 101 deletions(-) diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 542c67e38f..1596b0120a 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,7 +1,7 @@ use super::{ img_ids::{Imgs, ImgsRot}, Show, BLACK, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, - TEXT_COLOR_GREY, TRANSPARENT, UI_HIGHLIGHT_0, + TEXT_COLOR_GREY, UI_HIGHLIGHT_0, }; use crate::{ @@ -171,6 +171,9 @@ impl<'a> Widget for Group<'a> { .collect::>(); // Not considered in group for ui purposes if it is just pets let in_group = !group_members.is_empty(); + if !in_group { + self.show.group_menu = false; + } // Helper let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { @@ -238,7 +241,7 @@ impl<'a> Widget for Group<'a> { self.show.group_menu = !self.show.group_menu; }; // Member panels - let group_size = group_members.len() + 1; + let group_size = group_members.len(); if state.ids.member_panels_bg.len() < group_size { state.update(|s| { s.ids @@ -303,13 +306,7 @@ impl<'a> Widget for Group<'a> { .ecs() .read_resource::(); - for (i, &uid) in self - .client - .uid() - .iter() - .chain(group_members.iter().copied()) - .enumerate() - { + 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)); @@ -324,7 +321,7 @@ impl<'a> Widget for Group<'a> { } else { 110.0 }; - let pos = if i == 0 { + let back = if i == 0 { Image::new(self.imgs.member_bg) .top_left_with_margins_on(ui.window, offset, 20.0) } else { @@ -340,21 +337,13 @@ impl<'a> Widget for Group<'a> { }; // Don't show panel for the player! // Panel BG - pos.w_h(152.0, 36.0) - .color(if i == 0 { - Some(TRANSPARENT) - } else { - Some(TEXT_COLOR) - }) + back.w_h(152.0, 36.0) + .color(Some(TEXT_COLOR)) .set(state.ids.member_panels_bg[i], ui); // Health Image::new(self.imgs.bar_content) .w_h(148.0 * health_perc, 22.0) - .color(if i == 0 { - Some(TRANSPARENT) - } else { - Some(health_col) - }) + .color(Some(health_col)) .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0) .set(state.ids.member_health[i], ui); if stats.is_dead { @@ -363,7 +352,7 @@ impl<'a> Widget for Group<'a> { .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0) .font_size(20) .font_id(self.fonts.cyri.conrod_id) - .color(if i == 0 { TRANSPARENT } else { KILL_COLOR }) + .color(KILL_COLOR) .set(state.ids.dead_txt[i], ui); } else { // Health Text @@ -388,46 +377,34 @@ impl<'a> Widget for Group<'a> { .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset) .font_size(font_size) .font_id(self.fonts.cyri.conrod_id) - .color(if i == 0 { - TRANSPARENT - } else { - Color::Rgba(1.0, 1.0, 1.0, 0.5) - }) + .color(Color::Rgba(1.0, 1.0, 1.0, 0.5)) .set(state.ids.health_txt[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(if i == 0 { - Some(TRANSPARENT) - } else { - Some(UI_HIGHLIGHT_0) - }) + .color(Some(UI_HIGHLIGHT_0)) .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(if i == 0 { TRANSPARENT } else { BLACK }) + .color(BLACK) .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 i == 0 { TRANSPARENT } else { GROUP_COLOR }) + .color(GROUP_COLOR) .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(if i == 0 { - Some(TRANSPARENT) - } else { - Some(MANA_COLOR) - }) + .color(Some(MANA_COLOR)) .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0) .set(state.ids.member_stam[i], ui); } @@ -446,36 +423,28 @@ impl<'a> Widget for Group<'a> { } else { 110.0 }; - let pos = if i == 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) }; - pos.w_h(152.0, 36.0) - .color(if i == 0 { - Some(TRANSPARENT) - } else { - Some(TEXT_COLOR) - }) + 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(if i == 0 { - Some(TRANSPARENT) - } else { - Some(UI_HIGHLIGHT_0) - }) + .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(if i == 0 { TRANSPARENT } else { TEXT_COLOR }) + .color(TEXT_COLOR) .set(state.ids.dead_txt[i], ui); } } @@ -579,7 +548,7 @@ impl<'a> Widget for Group<'a> { } // Group Members, only character names, cut long names when they exceed the // button size - let group_size = group_members.len() + 1; + let group_size = group_members.len(); if state.ids.members.len() < group_size { state.update(|s| { s.ids @@ -598,13 +567,7 @@ impl<'a> Widget for Group<'a> { .rgba(0.33, 0.33, 0.33, 1.0) .set(state.ids.scrollbar, ui); // List member names - for (i, &uid) in self - .client - .uid() - .iter() - .chain(group_members.iter().copied()) - .enumerate() - { + for (i, &uid) in group_members.iter().copied().enumerate() { let selected = state.selected_member.map_or(false, |u| u == uid); let char_name = uid_to_name_text(uid, &self.client); diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index ab485be37c..4099b17186 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -82,7 +82,7 @@ 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 TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0); +//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0); //const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0); //const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0); diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 06e846e5f1..65c7982594 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -351,7 +351,11 @@ impl<'a> Widget for Social<'a> { }) }; // Create a name, level and zone row for every player in the list - for (i, (&uid, player_info)) in players.enumerate() { + // Filter out yourself from the online list + let my_uid = self.client.uid(); + for (i, (&uid, player_info)) in + players.filter(|(uid, _)| Some(**uid) != my_uid).enumerate() + { let selected = state.selected_uid.map_or(false, |u| u.0 == uid); let alias = &player_info.player_alias; let name = match &player_info.character { @@ -433,47 +437,63 @@ impl<'a> Widget for Social<'a> { { state.update(|s| s.selected_uid = Some((uid, Instant::now()))); } - let player = if name == self.stats.name { true } else { false }; - if Button::image(self.imgs.button) - .w_h(106.0, 26.0) - .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0) - .hover_image(if !player && selected { - self.imgs.button_hover - } else { - self.imgs.button - }) - .press_image(if !player && selected { - self.imgs.button_press - } else { - self.imgs.button - }) - .label(&self.localized_strings.get("hud.group.invite")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if !player && selected { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .image_color(if !player && selected { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.invite_button, ui) - .was_clicked() - { - if !player && selected { - events.push(Event::Invite(uid)); - state.update(|s| { - s.selected_uid = None; - }); - } + } + + // Invite Button + let selected_ingame = state + .selected_uid + .as_ref() + .map(|(s, _)| *s) + .filter(|selected| { + self.client + .player_list + .get(selected) + .map_or(false, |selected_player| { + selected_player.is_online && selected_player.character.is_some() + }) + }) + .or_else(|| { + self.selected_entity + .and_then(|s| self.client.state().read_component_copied(s.0)) + }); + + if Button::image(self.imgs.button) + .w_h(106.0, 26.0) + .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0) + .hover_image(if selected_ingame.is_some() { + self.imgs.button_hover + } else { + self.imgs.button + }) + .press_image(if selected_ingame.is_some() { + self.imgs.button_press + } else { + self.imgs.button + }) + .label(&self.localized_strings.get("hud.group.invite")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected_ingame.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .image_color(if selected_ingame.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.invite_button, ui) + .was_clicked() + { + if let Some(uid) = selected_ingame { + events.push(Event::Invite(uid)); + state.update(|s| { + s.selected_uid = None; + }); } } - // Invite Button - /* */ } // End of Online Tab // Alignment diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 3d2f84f834..e4e47c8c8e 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -537,6 +537,18 @@ impl PlayState for SessionState { self.target_entity.map(|e| (e, std::time::Instant::now())); } }, + Event::InputUpdate(GameInput::AcceptGroupInvite, true) => { + let mut client = self.client.borrow_mut(); + if client.group_invite().is_some() { + client.accept_group_invite(); + } + }, + Event::InputUpdate(GameInput::DeclineGroupInvite, true) => { + let mut client = self.client.borrow_mut(); + if client.group_invite().is_some() { + client.decline_group_invite(); + } + }, Event::AnalogGameInput(input) => match input { AnalogGameInput::MovementX(v) => { self.key_state.analog_matrix.x = v; From ecf6a84b2fbe6bd8335849703ff22473399c9eb1 Mon Sep 17 00:00:00 2001 From: Imbris Date: Fri, 31 Jul 2020 00:30:17 -0400 Subject: [PATCH 17/71] Don't give exp when killing self or group members --- server/src/events/entity_manipulation.rs | 110 +++++++++++++---------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index cdb3ee8ba0..b6900b70fd 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -55,56 +55,72 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc state.notify_registered_clients(comp::ChatType::Kill.server_msg(msg)); } - { - // Give EXP to the killer if entity had stats + // Give EXP to the killer if entity had stats + (|| { let mut stats = state.ecs().write_storage::(); - if let Some(entity_stats) = stats.get(entity).cloned() { - if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } = - cause - { - const MAX_EXP_DIST: f32 = 150.0; - // Attacker gets same as exp of everyone else - const ATTACKER_EXP_WEIGHT: f32 = 1.0; - let mut exp_reward = (entity_stats.body_type.base_exp() - + entity_stats.level.level() * entity_stats.body_type.base_exp_increase()) - as f32; - state.ecs().entity_from_uid(by.into()).map(|attacker| { - // Distribute EXP to group - let groups = state.ecs().read_storage::(); - let positions = state.ecs().read_storage::(); - // TODO: rework if change to groups makes it easier to iterate entities in a - // group - if let (Some(attacker_group), Some(pos)) = - (groups.get(attacker), positions.get(entity)) - { - let members_in_range = (&state.ecs().entities(), &groups, &positions) - .join() - .filter(|(entity, group, member_pos)| { - *group == attacker_group - && *entity != attacker - && pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) - }) - .map(|(entity, _, _)| entity) - .collect::>(); - let exp = - exp_reward / (members_in_range.len() as f32 + ATTACKER_EXP_WEIGHT); - exp_reward = exp * ATTACKER_EXP_WEIGHT; - members_in_range.into_iter().for_each(|e| { - if let Some(stats) = stats.get_mut(e) { - stats.exp.change_by(exp.ceil() as i64); - } - }); - } + let by = if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } = + cause + { + by + } else { + return; + }; + let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) { + attacker + } else { + return; + }; + let entity_stats = if let Some(entity_stats) = stats.get(entity) { + entity_stats + } else { + return; + }; - if let Some(attacker_stats) = stats.get_mut(attacker) { - // TODO: Discuss whether we should give EXP by Player - // Killing or not. - attacker_stats.exp.change_by(exp_reward.ceil() as i64); - } - }); - } + let groups = state.ecs().read_storage::(); + let attacker_group = groups.get(attacker); + let destroyed_group = groups.get(entity); + // Don't give exp if attacker destroyed themselves or one of their group members + if (attacker_group.is_some() && attacker_group == destroyed_group) || attacker == entity { + return; } - } + + // Maximum distance for other group members to receive exp + const MAX_EXP_DIST: f32 = 150.0; + // Attacker gets same as exp of everyone else + const ATTACKER_EXP_WEIGHT: f32 = 1.0; + let mut exp_reward = (entity_stats.body_type.base_exp() + + entity_stats.level.level() * entity_stats.body_type.base_exp_increase()) + as f32; + + // Distribute EXP to group + let positions = state.ecs().read_storage::(); + if let (Some(attacker_group), Some(pos)) = (attacker_group, positions.get(entity)) { + // TODO: rework if change to groups makes it easier to iterate entities in a + // group + let members_in_range = (&state.ecs().entities(), &groups, &positions) + .join() + .filter(|(entity, group, member_pos)| { + *group == attacker_group + && *entity != attacker + && pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) + }) + .map(|(entity, _, _)| entity) + .collect::>(); + let exp = exp_reward / (members_in_range.len() as f32 + ATTACKER_EXP_WEIGHT); + exp_reward = exp * ATTACKER_EXP_WEIGHT; + members_in_range.into_iter().for_each(|e| { + if let Some(stats) = stats.get_mut(e) { + stats.exp.change_by(exp.ceil() as i64); + } + }); + } + + if let Some(attacker_stats) = stats.get_mut(attacker) { + // TODO: Discuss whether we should give EXP by Player + // Killing or not. + attacker_stats.exp.change_by(exp_reward.ceil() as i64); + } + })(); if state .ecs() From 1eb671e1a6929986e58209a760d456320a8d9944 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Mon, 3 Aug 2020 01:53:02 +0200 Subject: [PATCH 18/71] Update CHANGELOG.md, german locale --- CHANGELOG.md | 4 + Cargo.lock | 32 ++- assets/common/loot_table.ron | 2 +- assets/voxygen/i18n/de_DE.ron | 20 +- assets/voxygen/i18n/en.ron | 14 +- .../voxel/weapon/staff/firestaff_cultist.vox | 4 +- client/src/lib.rs | 8 +- common/Cargo.toml | 2 +- common/src/comp/group.rs | 2 +- voxygen/src/hud/bag.rs | 4 +- voxygen/src/hud/crafting.rs | 19 +- voxygen/src/hud/group.rs | 41 +-- voxygen/src/hud/mod.rs | 15 +- voxygen/src/hud/settings_window.rs | 4 + voxygen/src/hud/social.rs | 251 +++--------------- world/src/site/dungeon/mod.rs | 2 +- 16 files changed, 153 insertions(+), 271 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c3409292..fcc83751c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Loading-Screen tips - Feeding animation for some animals - Power stat to weapons which affects weapon damage +- Add detection of entities under the cursor +- Functional group-system with exp-sharing and disabled damage to group members + ### Changed @@ -80,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed window resizing on Mac OS X. - Dehardcoded many item variants - Tooltips avoid the mouse better and disappear when hovered +- Improved social window functions and visuals ### Removed diff --git a/Cargo.lock b/Cargo.lock index 709ab9ea4b..bb5d2f2430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,6 +219,16 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "auth-common" +version = "0.1.0" +source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086" +dependencies = [ + "rand 0.7.3", + "serde", + "uuid", +] + [[package]] name = "auth-common" version = "0.1.0" @@ -229,12 +239,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "authc" +version = "1.0.0" +source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086" +dependencies = [ + "auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)", + "fxhash", + "hex", + "rust-argon2 0.8.2", + "serde_json", + "ureq", + "uuid", +] + [[package]] name = "authc" version = "1.0.0" source = "git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe#b943c85e4a38f5ec60cd18c34c73097640162bfe" dependencies = [ - "auth-common", + "auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", "fxhash", "hex", "rust-argon2 0.8.2", @@ -4601,7 +4625,7 @@ dependencies = [ name = "veloren-client" version = "0.6.0" dependencies = [ - "authc", + "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", "byteorder 1.3.4", "futures-executor", "futures-timer", @@ -4622,7 +4646,7 @@ name = "veloren-common" version = "0.6.0" dependencies = [ "arraygen", - "authc", + "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)", "criterion", "crossbeam", "dot_vox", @@ -4650,7 +4674,7 @@ dependencies = [ name = "veloren-server" version = "0.6.0" dependencies = [ - "authc", + "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", "chrono", "crossbeam", "diesel", diff --git a/assets/common/loot_table.ron b/assets/common/loot_table.ron index 0b00677652..59b741a6dc 100644 --- a/assets/common/loot_table.ron +++ b/assets/common/loot_table.ron @@ -88,7 +88,7 @@ (0.50, "common.items.weapons.staff.starter_staff"), (0.35, "common.items.weapons.staff.bone_staff"), (0.15, "common.items.weapons.staff.amethyst_staff"), - (0.01, "common.items.weapons.staff.cultist_staff"), + //(0.01, "common.items.weapons.staff.cultist_staff"), // hammers (0.05, "common.items.weapons.hammer.starter_hammer"), (0.05, "common.items.weapons.hammer.wood_hammer-0"), diff --git a/assets/voxygen/i18n/de_DE.ron b/assets/voxygen/i18n/de_DE.ron index de6f9305bb..a0aa8387b7 100644 --- a/assets/voxygen/i18n/de_DE.ron +++ b/assets/voxygen/i18n/de_DE.ron @@ -68,6 +68,7 @@ VoxygenLocalization( "common.none": "Kein", "common.error": "Fehler", "common.fatal_error": "Fataler Fehler", + "common.decline": "Ablehnen", /// End Common section // Message when connection to the server is lost @@ -306,7 +307,7 @@ magischen Gegenstände ergattern?"#, "hud.settings.unbound": "-", "hud.settings.reset_keybinds": "Auf Standard zurücksetzen", - "hud.social": "Sozial", + "hud.social": "Andere Spieler ", "hud.social.online": "Online", "hud.social.friends": "Freunde", "hud.social.not_yet_available": "Noch nicht verfügbar", @@ -314,6 +315,21 @@ magischen Gegenstände ergattern?"#, "hud.social.play_online_fmt": "{nb_player} Spieler online", "hud.spell": "Zauber", + + "hud.social.name" : "Name", + "hud.social.level" : "Lvl", + "hud.social.zone" : "Gebiet", + + "hud.group": "Gruppe", + "hud.group.invite": "Einladen", + "hud.group.leave": "Verlassen", + "hud.group.add_friend": "Freund hinzufügen", + "hud.group.assign_leader": "Anführer", + "hud.group.kick": "Entfernen", + "hud.group.invite_to_join": "{name} hat euch zu seiner Gruppe eingeladen!", + "hud.group.dead" : "Tot", + "hud.group.out_of_range": "Außer Reichweite", + "hud.group.link_group": "Gruppen verbinden", "hud.crafting": "Herstellen", "hud.crafting.recipes": "Rezepte", @@ -376,6 +392,8 @@ magischen Gegenstände ergattern?"#, "gameinput.freelook": "Freie Sicht", "gameinput.autowalk": "Automatisch Laufen", "gameinput.dance": "Tanzen", + "gameinput.declinegroupinvite": "Ablehnen", + "gameinput.acceptgroupinvite": "Annehmen", /// End GameInput section diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 302d545d7c..a5164163cb 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -71,6 +71,7 @@ VoxygenLocalization( "common.none": "None", "common.error": "Error", "common.fatal_error": "Fatal Error", + "common.you": "You", // Message when connection to the server is lost "common.connection_lost": r#"Connection lost! @@ -307,7 +308,7 @@ magically infused items?"#, "hud.settings.unbound": "None", "hud.settings.reset_keybinds": "Reset to Defaults", - "hud.social": "Social", + "hud.social": "Other Players", "hud.social.online": "Online:", "hud.social.friends": "Friends", "hud.social.not_yet_available": "Not yet available", @@ -325,13 +326,16 @@ magically infused items?"#, "hud.crafting.tool_cata": "Requires:", "hud.group": "Group", - "hud.group.invite_to_join": "{name} invited you to their group.", + "hud.group.invite_to_join": "{name} invited you to their group!", "hud.group.invite": "Invite", "hud.group.kick": "Kick", "hud.group.assign_leader": "Assign Leader", "hud.group.leave": "Leave Group", "hud.group.dead" : "Dead", "hud.group.out_of_range": "Out of range", + "hud.group.add_friend": "Add to Friends", + "hud.group.link_group": "Link Groups", + "hud.group.in_menu": "In Menu", "hud.spell": "Spells", @@ -453,7 +457,8 @@ Protection "Press 'F1' to see all default keybindings.", "You can type /say or /s to only chat with players directly around you.", "You can type /region or /r to only chat with players a couple of hundred blocks around you.", - "To send private message type /tell followed by a player name and your message.", + "You can type /group or /g to only chat with players in your current group.", + "To send private messages type /tell followed by a player name and your message.", "NPCs with the same level can have a different difficulty.", "Look at the ground for food, chests and other loot!", "Inventory filled with food? Try crafting better food from it!", @@ -464,7 +469,8 @@ Protection "Press 'J' to dance. Party!", "Press 'L-Shift' to open your Glider and conquer the skies.", "Veloren is still in Pre-Alpha. We do our best to improve it every day!", - "If you want to join the Dev-Team or just have a chat with us join our Discord-Server.", + "If you want to join the Dev-Team or just have a chat with us join our Discord-Server.", + "You can toggle showing your amount of health on the healthbar in the settings.", ], "npc.speech.villager_under_attack": [ "Help, I'm under attack!", diff --git a/assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox b/assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox index 6957a98086..2c27ee0143 100644 --- a/assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox +++ b/assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be510819e70d99ad4cb773de9a3cc8d343fc2d17236803e959b186a00020bd90 -size 27910 +oid sha256:39641c0dc427c30ac328101e3cbf7074e133d004a57ef964810b71d43779ddb8 +size 1464 diff --git a/client/src/lib.rs b/client/src/lib.rs index 1f459ed113..ee8d99224f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -17,8 +17,8 @@ use byteorder::{ByteOrder, LittleEndian}; use common::{ character::CharacterItem, comp::{ - self, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, - InventoryManip, InventoryUpdateEvent, group, + self, group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, + InventoryManip, InventoryUpdateEvent, }, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, @@ -434,7 +434,9 @@ impl Client { pub fn group_invite(&self) -> Option { self.group_invite } - pub fn group_info(&self) -> Option<(String, Uid)> { self.group_leader.map(|l| ("TODO".into(), l)) } + pub fn group_info(&self) -> Option<(String, Uid)> { + self.group_leader.map(|l| ("Group".into(), l)) // TODO + } pub fn group_members(&self) -> &HashMap { &self.group_members } diff --git a/common/Cargo.toml b/common/Cargo.toml index 528fd962be..92d63c8045 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -29,7 +29,7 @@ crossbeam = "0.7" notify = "5.0.0-pre.3" indexmap = "1.3.0" sum_type = "0.2.0" -authc = { git = "https://gitlab.com/veloren/auth.git", rev = "223a4097f7ebc8d451936dccb5e6517194bbf086" } +authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" } slab = "0.4.2" [dev-dependencies] diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 2fcc570467..9c38f3d74f 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -137,7 +137,7 @@ impl GroupManager { fn create_group(&mut self, leader: specs::Entity) -> Group { Group(self.groups.insert(GroupInfo { leader, - name: "Flames".into(), + name: "Group".into(), }) as u32) } diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 0af0b8b244..992232de94 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -229,7 +229,7 @@ impl<'a> Widget for Bag<'a> { ) .mid_top_with_margin_on(state.ids.bg_frame, 9.0) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) + .font_size(self.fonts.cyri.scale(20)) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .set(state.ids.inventory_title_bg, ui); Text::new( @@ -240,7 +240,7 @@ impl<'a> Widget for Bag<'a> { ) .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) + .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) .set(state.ids.inventory_title, ui); // Scrollbar-BG diff --git a/voxygen/src/hud/crafting.rs b/voxygen/src/hud/crafting.rs index 19fff3dfa6..99c706a5dc 100644 --- a/voxygen/src/hud/crafting.rs +++ b/voxygen/src/hud/crafting.rs @@ -117,23 +117,6 @@ impl<'a> Widget for Crafting<'a> { ) }); } - /*if state.ids.recipe_img_frame.len() < self.client.recipe_book().iter().len() { - state.update(|state| { - state.ids.recipe_img_frame.resize( - self.client.recipe_book().iter().len(), - &mut ui.widget_id_generator(), - ) - }); - } - if state.ids.recipe_img.len() < self.client.recipe_book().iter().len() { - state.update(|state| { - state.ids.recipe_img.resize( - self.client.recipe_book().iter().len(), - &mut ui.widget_id_generator(), - ) - }); - }*/ - let ids = &state.ids; let mut events = Vec::new(); @@ -186,7 +169,7 @@ impl<'a> Widget for Crafting<'a> { Text::new(&self.localized_strings.get("hud.crafting")) .mid_top_with_margin_on(ids.window_frame, 9.0) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) + .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) .set(ids.title_main, ui); diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 1596b0120a..c39dbb9301 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -205,7 +205,7 @@ impl<'a> Widget for Group<'a> { // broken if self.show.group_menu || open_invite.is_some() { // Frame - Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) + Rectangle::fill_with([220.0, 165.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) .bottom_left_with_margins_on(ui.window, 220.0, 10.0) .set(state.ids.bg, ui); } @@ -358,15 +358,17 @@ impl<'a> Widget for Group<'a> { // Health Text let txt = format!( "{}/{}", - stats.health.current() as u32, - stats.health.maximum() as u32, + stats.health.current() / 10 as u32, + stats.health.maximum() / 10 as u32, ); + // Change font size depending on health amount let font_size = match stats.health.maximum() { 0..=999 => 14, 1000..=9999 => 13, 10000..=99999 => 12, _ => 11, }; + // Change text offset depending on health amount let txt_offset = match stats.health.maximum() { 0..=999 => 4.0, 1000..=9999 => 4.5, @@ -458,16 +460,17 @@ impl<'a> Widget for Group<'a> { .color(TEXT_COLOR) .set(state.ids.title, ui); if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label("Add to Friends") - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_friend, ui) - .was_clicked() + .w_h(90.0, 22.0) + .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) + .hover_image(self.imgs.button) // Change this when the friendslist is working + .press_image(self.imgs.button) // Change this when the friendslist is working + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .image_color (TEXT_COLOR_GREY) // Change this when the friendslist is working + .label(&self.localized_strings.get("hud.group.add_friend")) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_friend, ui) + .was_clicked() {}; if Button::image(self.imgs.button) .w_h(90.0, 22.0) @@ -482,6 +485,7 @@ impl<'a> Widget for Group<'a> { .was_clicked() { self.show.group_menu = false; + self.show.group = !self.show.group; events.push(Event::LeaveGroup); }; // Group leader functions @@ -514,8 +518,11 @@ impl<'a> Widget for Group<'a> { .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) .hover_image(self.imgs.button) .press_image(self.imgs.button) - .label("Link Group") // TODO: Localize - .label_color(TEXT_COLOR_GREY) // Change this when the linking is working + .label(&self.localized_strings.get("hud.group.link_group")) + .hover_image(self.imgs.button) // Change this when the friendslist is working + .press_image(self.imgs.button) // Change this when the friendslist is working + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .image_color (TEXT_COLOR_GREY) // Change this when the friendslist is working .label_font_id(self.fonts.cyri.conrod_id) .label_font_size(self.fonts.cyri.scale(10)) .set(state.ids.btn_link, ui) @@ -557,7 +564,7 @@ impl<'a> Widget for Group<'a> { }) } // Scrollable area for group member names - Rectangle::fill_with([110.0, 192.0], color::TRANSPARENT) + Rectangle::fill_with([110.0, 135.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) .scroll_kids() .scroll_kids_vertically() @@ -582,7 +589,7 @@ impl<'a> Widget for Group<'a> { if i == 0 { w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0) } else { - w.down_from(state.ids.members[i - 1], 10.0) + w.down_from(state.ids.members[i - 1], 5.0) } }) .hover_image(self.imgs.selection_hover) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 4099b17186..fdb13ab68a 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -409,7 +409,6 @@ impl Show { fn social(&mut self, open: bool) { if !self.esc_menu { self.social = open; - self.crafting = false; self.spell = false; self.want_grab = !open; } @@ -509,7 +508,7 @@ impl Show { } fn toggle_social(&mut self) { - self.social = !self.social; + self.social(!self.social); self.spell = false; } @@ -1925,22 +1924,25 @@ impl Hud { // Social Window if self.show.social { let ecs = client.state().ecs(); - let stats = ecs.read_storage::(); + let _stats = ecs.read_storage::(); let me = client.entity(); - if let Some(stats) = stats.get(me) { + if let Some(_stats) = stats.get(me) { for event in Social::new( &self.show, client, &self.imgs, &self.fonts, &self.voxygen_i18n, - &stats, + //&stats, info.selected_entity, ) .set(self.ids.social_window, ui_widgets) { match event { - social::Event::Close => self.show.social(false), + social::Event::Close => { + self.show.social(false); + self.force_ungrab = true; + }, social::Event::ChangeSocialTab(social_tab) => { self.show.open_social_tab(social_tab) }, @@ -1950,7 +1952,6 @@ impl Hud { } } // Group Window - for event in Group::new( &mut self.show, client, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 9c92324eb3..06c1b24e94 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -1190,6 +1190,10 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.chat_char_name_text, ui); + // Show account name in chat + + // Show account names in social window + // Language select drop down Text::new(&self.localized_strings.get("common.languages")) .down_from(state.ids.chat_char_name_button, 20.0) diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 65c7982594..b340db8440 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -2,7 +2,7 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MA use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; -use common::{comp::Stats, sync::Uid}; +use common::sync::Uid; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, @@ -14,6 +14,7 @@ widget_ids! { pub struct Ids { frame, close, + title_align, title, bg, icon, @@ -62,8 +63,7 @@ pub struct Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - stats: &'a Stats, - + //stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, #[conrod(common_builder)] @@ -77,7 +77,7 @@ impl<'a> Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - stats: &'a Stats, + //stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { @@ -86,7 +86,7 @@ impl<'a> Social<'a> { imgs, fonts, localized_strings, - stats, + //stats, selected_entity, common: widget::CommonBuilder::default(), } @@ -120,7 +120,11 @@ impl<'a> Widget for Social<'a> { let mut events = Vec::new(); // Window frame and BG - let pos = if self.show.group { 180.0 } else { 25.0 }; + let pos = if self.show.group || self.show.group_menu { + 200.0 + } else { + 25.0 + }; // TODO: Different window visuals depending on the selected tab let window_bg = match &self.show.social_tab { SocialTab::Online => self.imgs.social_bg_on, @@ -133,7 +137,7 @@ impl<'a> Widget for Social<'a> { SocialTab::Faction => self.imgs.social_frame_fact, }; Image::new(window_bg) - .top_left_with_margins_on(ui.window, 200.0, pos) + .bottom_left_with_margins_on(ui.window, 308.0, pos) .color(Some(UI_MAIN)) .w_h(280.0, 460.0) .set(state.ids.bg, ui); @@ -160,10 +164,13 @@ impl<'a> Widget for Social<'a> { } // Title + Rectangle::fill_with([212.0, 42.0], color::TRANSPARENT) + .top_left_with_margins_on(state.ids.frame, 2.0, 44.0) + .set(state.ids.title_align, ui); Text::new(&self.localized_strings.get("hud.social")) - .mid_top_with_margin_on(state.ids.frame, 9.0) + .middle_of(state.ids.title_align) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) + .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) .set(state.ids.title, ui); @@ -186,7 +193,7 @@ impl<'a> Widget for Social<'a> { SocialTab::Online => UI_MAIN, _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), }) - .top_right_with_margins_on(state.ids.frame, 50.0, -28.0) + .top_right_with_margins_on(state.ids.frame, 50.0, -27.0) .set(state.ids.online_tab, ui) .was_clicked() { @@ -322,7 +329,7 @@ impl<'a> Widget for Social<'a> { .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) .set(state.ids.online_txt, ui); - Text::new(&count.to_string()) + Text::new(&(count - 1).to_string()) .right_from(state.ids.online_txt, 5.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) @@ -356,24 +363,33 @@ impl<'a> Widget for Social<'a> { for (i, (&uid, player_info)) in players.filter(|(uid, _)| Some(**uid) != my_uid).enumerate() { + let hide_username = true; + let zone = "Wilderness"; // TODO Add real zone let selected = state.selected_uid.map_or(false, |u| u.0 == uid); let alias = &player_info.player_alias; - let name = match &player_info.character { - Some(character) => format!("{} ", &character.name), - None => "".to_string(), // character select or spectating + let name_text = match &player_info.character { + Some(character) => { + if Some(uid) == my_uid { + format!( + "{} ({})", + &self.localized_strings.get("hud.common.you"), + &character.name + ) + } else if hide_username { + character.name.clone() + } else { + format!("[{}] {}", alias, &character.name) + } + }, + None => alias.clone(), // character select or spectating }; let level = match &player_info.character { Some(character) => format!("{} ", &character.level), - None => "".to_string(), // character select or spectating + None => "".to_string(), // character select or spectating }; - let setting = true; // TODO Remove this - let zone = "Wilderness"; // TODO: Add real zone - let name_text = if name == self.stats.name { - format!("You ({})", name) // TODO: Locale - } else if setting { - format!("{}", name) - } else { - format!("[{}] {}", alias, name) + let zone_name = match &player_info.character { + None => self.localized_strings.get("hud.group.in_menu").to_string(), /* character select or spectating */ + _ => format!("{} ", &zone), }; // Player name widgets let button = Button::image(if !selected { @@ -417,9 +433,9 @@ impl<'a> Widget for Social<'a> { .color(TEXT_COLOR) .set(state.ids.player_levels[i], ui); let zone_txt = if i == 0 { - Text::new(&zone).mid_top_with_margin_on(state.ids.zones_align, 2.0) + Text::new(&zone_name).mid_top_with_margin_on(state.ids.zones_align, 2.0) } else { - Text::new(&zone).down_from(state.ids.player_zones[i - 1], 2.0) + Text::new(&zone_name).down_from(state.ids.player_zones[i - 1], 2.0) }; zone_txt .font_size(self.fonts.cyri.scale(14)) @@ -494,190 +510,7 @@ impl<'a> Widget for Social<'a> { }); } } - } // End of Online Tab - - // Alignment - /* - // Online Tab - - // Contents - - if let SocialTab::Online = self.show.social_tab { - // Players list - // TODO: this list changes infrequently enough that it should not have to be - // recreated every frame - let players = self.client.player_list.iter().filter(|(_, p)| p.is_online); - let count = players.clone().count(); - if state.ids.player_names.len() < count { - state.update(|s| { - s.ids - .player_names - .resize(count, &mut ui.widget_id_generator()) - }) - } - Text::new( - &self - .localized_strings - .get("hud.social.play_online_fmt") - .replace("{nb_player}", &format!("{:?}", count)), - ) - .top_left_with_margins_on(state.ids.content_align, -2.0, 7.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.online_title, ui); - - // Clear selected player if an entity was selected - if state - .selected_uid - .zip(self.selected_entity) - // Compare instants - .map_or(false, |(u, e)| u.1 < e.1) - { - state.update(|s| s.selected_uid = None); - } - - for (i, (&uid, player_info)) in players.enumerate() { - let selected = state.selected_uid.map_or(false, |u| u.0 == uid); - let alias = &player_info.player_alias; - let character_name_level = match &player_info.character { - Some(character) => format!("{} Lvl {}", &character.name, &character.level), - None => "".to_string(), // character select or spectating - }; - let text = if selected { - format!("-> [{}] {}", alias, character_name_level) - } else { - format!("[{}] {}", alias, character_name_level) - }; - Text::new(&text) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.player_names[i], ui); - // Check for click - if ui - .widget_input(state.ids.player_names[i]) - .clicks() - .left() - .next() - .is_some() - { - state.update(|s| s.selected_uid = Some((uid, Instant::now()))); - } - } - - // Invite Button - if self - .client - .group_info() - .map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid)) - { - let selected = state.selected_uid.map(|s| s.0).or_else(|| { - self.selected_entity - .and_then(|s| self.client.state().read_component_copied(s.0)) - }); - - if Button::image(self.imgs.button) - .down(3.0) - .w_h(150.0, 30.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(&self.localized_strings.get("hud.group.invite")) - .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected.is_some() { - TEXT_COLOR - } else { - TEXT_COLOR_3 - }) - .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.invite_button, ui) - .was_clicked() - { - if let Some(uid) = selected { - events.push(Event::Invite(uid)); - state.update(|s| { - s.selected_uid = None; - }); - } - } - } - } - - // Friends Tab - - if Button::image(if let SocialTab::Friends = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button - }) - .w_h(30.0 * 4.0, 12.0 * 4.0) - .hover_image(if let SocialTab::Friends = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button - }) - .press_image(if let SocialTab::Friends = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button - }) - .right_from(state.ids.online_tab, 0.0) - .label(&self.localized_strings.get("hud.social.friends")) - .label_font_size(self.fonts.cyri.scale(14)) - .label_font_id(self.fonts.cyri.conrod_id) - .parent(state.ids.frame) - .color(UI_MAIN) - .label_color(TEXT_COLOR_3) - .set(state.ids.friends_tab, ui) - .was_clicked() - { - events.push(Event::ChangeSocialTab(SocialTab::Friends)); - } - - // Contents - - if let SocialTab::Friends = self.show.social_tab { - Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(state.ids.content_align) - .font_size(self.fonts.cyri.scale(18)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR_3) - .set(state.ids.friends_test, ui); - } - - // Faction Tab - let button_img = if let SocialTab::Faction = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button - }; - if Button::image(button_img) - .w_h(30.0 * 4.0, 12.0 * 4.0) - .right_from(state.ids.friends_tab, 0.0) - .label(&self.localized_strings.get("hud.social.faction")) - .parent(state.ids.frame) - .label_font_size(self.fonts.cyri.scale(14)) - .label_font_id(self.fonts.cyri.conrod_id) - .color(UI_MAIN) - .label_color(TEXT_COLOR_3) - .set(state.ids.faction_tab, ui) - .was_clicked() - { - events.push(Event::ChangeSocialTab(SocialTab::Faction)); - } - - // Contents - - if let SocialTab::Faction = self.show.social_tab { - Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(state.ids.content_align) - .font_size(self.fonts.cyri.scale(18)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR_3) - .set(state.ids.faction_test, ui); - }*/ + } // End of Online Tab events } diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 34d6a0aebf..10cecd7807 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -522,7 +522,7 @@ impl Floor { "common.items.armor.shoulder.cultist_shoulder_purple", ), 8 => comp::Item::expect_from_asset( - "common.items.weapons.sword.greatsword_2h_fine-0", + "common.items.weapons.staff.cultist_staff", ), 9 => comp::Item::expect_from_asset( "common.items.weapons.sword.greatsword_2h_fine-1", From 3da7e27a7c7727bb1ad029b8850a21461db235c9 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Mon, 3 Aug 2020 21:42:06 +0200 Subject: [PATCH 19/71] overhead info improvements overhead improvements --- voxygen/src/hud/group.rs | 2 + voxygen/src/hud/overhead.rs | 208 +++++++++++++++++++++--------------- voxygen/src/hud/skillbar.rs | 36 ++++--- voxygen/src/hud/social.rs | 14 +-- 4 files changed, 154 insertions(+), 106 deletions(-) diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index c39dbb9301..63c70bb2dc 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -173,6 +173,7 @@ impl<'a> Widget for Group<'a> { let in_group = !group_members.is_empty(); if !in_group { self.show.group_menu = false; + self.show.group = false; } // Helper @@ -207,6 +208,7 @@ impl<'a> Widget for Group<'a> { // Frame Rectangle::fill_with([220.0, 165.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) .bottom_left_with_margins_on(ui.window, 220.0, 10.0) + .crop_kids() .set(state.ids.bg, ui); } if open_invite.is_some() { diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index abcf9ebdd5..f3079b1630 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -42,6 +42,7 @@ widget_ids! { level_skull, health_bar, health_bar_bg, + health_txt, mana_bar, health_bar_fg, } @@ -62,6 +63,7 @@ pub struct Overhead<'a> { voxygen_i18n: &'a std::sync::Arc, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, + #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -107,13 +109,20 @@ impl<'a> Ingameable for Overhead<'a> { // Number of conrod primitives contained in the overhead display. TODO maybe // this could be done automatically? // - 2 Text::new for name + // If HP Info is shown + 6 // - 1 for level: either Text or Image // - 4 for HP + mana + fg + bg - // If there's a speech bubble - // - 2 Text::new for speech bubble + // - 1 for HP Text + // If there's a speech bubble + 13 + // - 2 Text::new for speec8 bubble // - 1 Image::new for icon // - 10 Image::new for speech bubble (9-slice + tail) - 7 + if self.bubble.is_some() { 13 } else { 0 } + 2 + if self.bubble.is_some() { 13 } else { 0 } + + if (self.stats.health.current() as f64 / self.stats.health.maximum() as f64) < 1.0 { + 6 + } else { + 0 + } } } @@ -137,23 +146,32 @@ impl<'a> Widget for Overhead<'a> { const BARSIZE: f64 = 2.0; const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5; const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0; - + let hp_percentage = + self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; + let name_y = if hp_percentage == 100.0 { + MANA_BAR_Y + 10.0 + } else { + MANA_BAR_Y + 32.0 + }; + let font_size = if hp_percentage == 100.0 { 24 } else { 20 }; // Name Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) - .font_size(30) + .font_size(font_size) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .x_y(-1.0, MANA_BAR_Y + 48.0) + .x_y(-1.0, name_y) + .parent(id) .set(state.ids.name_bg, ui); Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) - .font_size(30) + .font_size(font_size) .color(if self.in_group { GROUP_MEMBER } else { DEFAULT_NPC }) - .x_y(0.0, MANA_BAR_Y + 50.0) + .x_y(0.0, name_y + 1.0) + .parent(id) .set(state.ids.name, ui); // Speech bubble @@ -167,7 +185,7 @@ impl<'a> Widget for Overhead<'a> { .color(text_color) .font_id(self.fonts.cyri.conrod_id) .font_size(18) - .up_from(state.ids.name, 20.0) + .up_from(state.ids.name, 26.0) .x_align_to(state.ids.name, Align::Middle) .parent(id); @@ -302,101 +320,121 @@ impl<'a> Widget for Overhead<'a> { .set(state.ids.speech_bubble_icon, ui); } - let hp_percentage = - self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; - let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer - let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); + if hp_percentage < 100.0 { + // Show HP Bar + let hp_percentage = + self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; + let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer + let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); - // Background - Image::new(self.imgs.enemy_health_bg) + // Background + Image::new(self.imgs.enemy_health_bg) .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8))) .parent(id) .set(state.ids.health_bar_bg, ui); - // % HP Filling - Image::new(self.imgs.enemy_bar) - .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE) - .x_y( - (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE, - MANA_BAR_Y + 7.5, - ) - .color(Some(if hp_percentage <= 25.0 { - crit_hp_color - } else if hp_percentage <= 50.0 { - LOW_HP_COLOR - } else { - HP_COLOR - })) - .parent(id) - .set(state.ids.health_bar, ui); + // % HP Filling + Image::new(self.imgs.enemy_bar) + .w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE) + .x_y( + (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE, + MANA_BAR_Y + 7.5, + ) + .color(Some(if hp_percentage <= 25.0 { + crit_hp_color + } else if hp_percentage <= 50.0 { + LOW_HP_COLOR + } else { + HP_COLOR + })) + .parent(id) + .set(state.ids.health_bar, ui); + // TODO Only show health values for entities below 100% health + let mut txt = format!( + "{}/{}", + self.stats.health.current().max(1) / 10 as u32, /* Don't show 0 health for + * living entities */ + self.stats.health.maximum() / 10 as u32, + ); + if self.stats.is_dead { + txt = self.voxygen_i18n.get("hud.group.dead").to_string() + }; + Text::new(&txt) + .mid_top_with_margin_on(state.ids.health_bar_bg, 2.0) + .font_size(10) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .parent(id) + .set(state.ids.health_txt, ui); - // % Mana Filling - if let Some(energy) = self.energy { - let energy_factor = energy.current() as f64 / energy.maximum() as f64; + // % Mana Filling + if let Some(energy) = self.energy { + let energy_factor = energy.current() as f64 / energy.maximum() as f64; - Rectangle::fill_with( - [72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT], - MANA_COLOR, - ) - .x_y( - ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE, - MANA_BAR_Y, //-32.0, - ) - .parent(id) - .set(state.ids.mana_bar, ui); - } + Rectangle::fill_with( + [72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT], + MANA_COLOR, + ) + .x_y( + ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE, + MANA_BAR_Y, //-32.0, + ) + .parent(id) + .set(state.ids.mana_bar, ui); + } - // Foreground - Image::new(self.imgs.enemy_health) + // Foreground + Image::new(self.imgs.enemy_health) .w_h(84.0 * BARSIZE, 10.0 * BARSIZE) .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5) .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99))) .parent(id) .set(state.ids.health_bar_fg, ui); - // Level - const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4); - const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); - const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); - // Change visuals of the level display depending on the player level/opponent - // level - let level_comp = self.stats.level.level() as i64 - self.own_level as i64; - // + 10 level above player -> skull - // + 5-10 levels above player -> high - // -5 - +5 levels around player level -> equal - // - 5 levels below player -> low - if level_comp > 9 { - let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer - Image::new(if skull_ani as i32 == 1 && rand::random::() < 0.9 { - self.imgs.skull_2 - } else { - self.imgs.skull - }) - .w_h(18.0 * BARSIZE, 18.0 * BARSIZE) - .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) - .parent(id) - .set(state.ids.level_skull, ui); - } else { - Text::new(&format!("{}", self.stats.level.level())) - .font_id(self.fonts.cyri.conrod_id) - .font_size(if self.stats.level.level() > 9 && level_comp < 10 { - 14 + // Level + const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4); + const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); + const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); + // Change visuals of the level display depending on the player level/opponent + // level + let level_comp = self.stats.level.level() as i64 - self.own_level as i64; + // + 10 level above player -> skull + // + 5-10 levels above player -> high + // -5 - +5 levels around player level -> equal + // - 5 levels below player -> low + if level_comp > 9 { + let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer + Image::new(if skull_ani as i32 == 1 && rand::random::() < 0.9 { + self.imgs.skull_2 } else { - 15 + self.imgs.skull }) - .color(if level_comp > 4 { - HIGH - } else if level_comp < -5 { - LOW - } else { - EQUAL - }) - .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0) + .w_h(18.0 * BARSIZE, 18.0 * BARSIZE) + .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) .parent(id) - .set(state.ids.level, ui); + .set(state.ids.level_skull, ui); + } else { + Text::new(&format!("{}", self.stats.level.level())) + .font_id(self.fonts.cyri.conrod_id) + .font_size(if self.stats.level.level() > 9 && level_comp < 10 { + 14 + } else { + 15 + }) + .color(if level_comp > 4 { + HIGH + } else if level_comp < -5 { + LOW + } else { + EQUAL + }) + .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0) + .parent(id) + .set(state.ids.level, ui); + } } } } diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index f365473e1d..9b0654eb91 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -219,10 +219,14 @@ impl<'a> Widget for Skillbar<'a> { let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64); - let hp_percentage = + let mut hp_percentage = self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; - let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0; - + let mut energy_percentage = + self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0; + if self.stats.is_dead { + hp_percentage = 0.0; + energy_percentage = 0.0; + }; let scale = 2.0; let bar_values = self.global_state.settings.gameplay.bar_numbers; @@ -1160,14 +1164,14 @@ impl<'a> Widget for Skillbar<'a> { }; Image::new(self.imgs.bar_content) .w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale) - .color(Some(health_col)) + .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); + .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) @@ -1180,11 +1184,21 @@ impl<'a> Widget for Skillbar<'a> { // Bar Text // Values if let BarNumbers::Values = bar_values { - let hp_text = format!( + let mut hp_text = format!( "{}/{}", - (self.stats.health.current() / 10) as u32, + (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)) @@ -1196,13 +1210,7 @@ impl<'a> Widget for Skillbar<'a> { .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!( - "{}/{}", - 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 - ); + .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)) diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index b340db8440..ee875b26e5 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -262,15 +262,15 @@ impl<'a> Widget for Social<'a> { .mid_top_with_margin_on(state.ids.frame, 74.0) .scroll_kids_vertically() .set(state.ids.online_align, ui); - Rectangle::fill_with([133.0, 370.0], color::TRANSPARENT) + Rectangle::fill_with([133.0, 346.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.online_align, 0.0, 0.0) .crop_kids() .set(state.ids.names_align, ui); - Rectangle::fill_with([39.0, 370.0], color::TRANSPARENT) + Rectangle::fill_with([39.0, 346.0], color::TRANSPARENT) .right_from(state.ids.names_align, 2.0) .crop_kids() .set(state.ids.levels_align, ui); - Rectangle::fill_with([94.0, 370.0], color::TRANSPARENT) + Rectangle::fill_with([94.0, 346.0], color::TRANSPARENT) .right_from(state.ids.levels_align, 2.0) .crop_kids() .set(state.ids.zones_align, ui); @@ -423,9 +423,9 @@ impl<'a> Widget for Social<'a> { .was_clicked() {}; let level_txt = if i == 0 { - Text::new(&level).mid_top_with_margin_on(state.ids.levels_align, 2.0) + Text::new(&level).mid_top_with_margin_on(state.ids.levels_align, 4.0) } else { - Text::new(&level).down_from(state.ids.player_levels[i - 1], 2.0) + Text::new(&level).down_from(state.ids.player_levels[i - 1], 4.0) }; level_txt .font_size(self.fonts.cyri.scale(14)) @@ -433,9 +433,9 @@ impl<'a> Widget for Social<'a> { .color(TEXT_COLOR) .set(state.ids.player_levels[i], ui); let zone_txt = if i == 0 { - Text::new(&zone_name).mid_top_with_margin_on(state.ids.zones_align, 2.0) + Text::new(&zone_name).mid_top_with_margin_on(state.ids.zones_align, 4.0) } else { - Text::new(&zone_name).down_from(state.ids.player_zones[i - 1], 2.0) + Text::new(&zone_name).down_from(state.ids.player_zones[i - 1], 4.0) }; zone_txt .font_size(self.fonts.cyri.scale(14)) From 543d971a1942184e76fae15d5a9bbd340b9a5a15 Mon Sep 17 00:00:00 2001 From: Imbris Date: Mon, 3 Aug 2020 17:54:33 -0400 Subject: [PATCH 20/71] Projectiles ignore entities in the same group, pets no longer follow group leader --- common/src/sys/agent.rs | 18 +++++++++++------- common/src/sys/phys.rs | 37 ++++++++++++++++++++++++++++++------ common/src/sys/projectile.rs | 28 ++++++++------------------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 5f3fa14869..51e31a7f14 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -113,13 +113,17 @@ impl<'a> System<'a> for Sys { .join() { // Hack, replace with better system when groups are more sophisticated - // Override alignment if in a group - let alignment = group - .and_then(|g| group_manager.group_info(*g)) - .and_then(|info| uids.get(info.leader)) - .copied() - .map(Alignment::Owned) - .or(alignment.copied()); + // Override alignment if in a group unless entity is owned already + let alignment = if !matches!(alignment, Some(Alignment::Owned(_))) { + group + .and_then(|g| group_manager.group_info(*g)) + .and_then(|info| uids.get(info.leader)) + .copied() + .map(Alignment::Owned) + .or(alignment.copied()) + } else { + alignment.copied() + }; // Skip mounted entities if mount_state diff --git a/common/src/sys/phys.rs b/common/src/sys/phys.rs index c065a9621f..ffec0df1b4 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -1,12 +1,17 @@ use crate::{ - comp::{Collider, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, Scale, Sticky, Vel}, + comp::{ + Collider, Gravity, Group, Mass, Mounting, Ori, PhysicsState, Pos, Projectile, Scale, + Sticky, Vel, + }, event::{EventBus, ServerEvent}, state::DeltaTime, - sync::Uid, + sync::{Uid, UidAllocator}, terrain::{Block, BlockKind, TerrainGrid}, vol::ReadVol, }; -use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; +use specs::{ + saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage, +}; use vek::*; pub const GRAVITY: f32 = 9.81 * 5.0; @@ -44,6 +49,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Uid>, ReadExpect<'a, TerrainGrid>, Read<'a, DeltaTime>, + Read<'a, UidAllocator>, Read<'a, EventBus>, ReadStorage<'a, Scale>, ReadStorage<'a, Sticky>, @@ -55,6 +61,8 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, ReadStorage<'a, Mounting>, + ReadStorage<'a, Group>, + ReadStorage<'a, Projectile>, ); #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -66,6 +74,7 @@ impl<'a> System<'a> for Sys { uids, terrain, dt, + uid_allocator, event_bus, scales, stickies, @@ -77,6 +86,8 @@ impl<'a> System<'a> for Sys { mut velocities, mut orientations, mountings, + groups, + projectiles, ): Self::SystemData, ) { let mut event_emitter = event_bus.emitter(); @@ -432,7 +443,7 @@ impl<'a> System<'a> for Sys { } // Apply pushback - for (pos, scale, mass, vel, _, _, _, physics) in ( + for (pos, scale, mass, vel, _, _, _, physics, projectile) in ( &positions, scales.maybe(), masses.maybe(), @@ -441,9 +452,12 @@ impl<'a> System<'a> for Sys { !&mountings, stickies.maybe(), &mut physics_states, + // TODO: if we need to avoid collisions for other things consider moving whether it + // should interact into the collider component or into a separate component + projectiles.maybe(), ) .join() - .filter(|(_, _, _, _, _, _, sticky, physics)| { + .filter(|(_, _, _, _, _, _, sticky, physics, _)| { sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground) }) { @@ -452,16 +466,27 @@ impl<'a> System<'a> for Sys { let scale = scale.map(|s| s.0).unwrap_or(1.0); let mass = mass.map(|m| m.0).unwrap_or(scale); - for (other, pos_other, scale_other, mass_other, _, _) in ( + // Group to ignore collisions with + let ignore_group = projectile + .and_then(|p| p.owner) + .and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into())) + .and_then(|e| groups.get(e)); + + for (other, pos_other, scale_other, mass_other, _, _, group) in ( &uids, &positions, scales.maybe(), masses.maybe(), &colliders, !&mountings, + groups.maybe(), ) .join() { + if ignore_group.is_some() && ignore_group == group { + continue; + } + let scale_other = scale_other.map(|s| s.0).unwrap_or(1.0); let mass_other = mass_other.map(|m| m.0).unwrap_or(scale_other); diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs index ad2e4a1023..ae6817cf35 100644 --- a/common/src/sys/projectile.rs +++ b/common/src/sys/projectile.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - projectile, Alignment, Damage, DamageSource, Energy, EnergySource, HealthChange, - HealthSource, Loadout, Ori, PhysicsState, Pos, Projectile, Vel, + projectile, Damage, DamageSource, Energy, EnergySource, HealthChange, HealthSource, + Loadout, Ori, PhysicsState, Pos, Projectile, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, state::DeltaTime, @@ -28,7 +28,6 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Ori>, WriteStorage<'a, Projectile>, WriteStorage<'a, Energy>, - ReadStorage<'a, Alignment>, ReadStorage<'a, Loadout>, ); @@ -46,7 +45,6 @@ impl<'a> System<'a> for Sys { mut orientations, mut projectiles, mut energies, - alignments, loadouts, ): Self::SystemData, ) { @@ -92,23 +90,13 @@ impl<'a> System<'a> for Sys { healthchange: healthchange as f32, source: DamageSource::Projectile, }; - if let Some(entity) = - uid_allocator.retrieve_entity_internal(other.into()) - { - if let Some(loadout) = loadouts.get(entity) { - damage.modify_damage(false, loadout); - } + + let other_entity = uid_allocator.retrieve_entity_internal(other.into()); + if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) { + damage.modify_damage(false, loadout); } - // Hacky: remove this when groups get implemented - let passive = uid_allocator - .retrieve_entity_internal(other.into()) - .and_then(|other| { - alignments - .get(other) - .map(|a| Alignment::Owned(owner_uid).passive_towards(*a)) - }) - .unwrap_or(false); - if other != projectile.owner.unwrap() && !passive { + + if other != owner_uid { server_emitter.emit(ServerEvent::Damage { uid: other, change: HealthChange { From 28a8f847cc8995aca436a5fd7408f4c37263c183 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Tue, 4 Aug 2020 09:22:59 +0200 Subject: [PATCH 21/71] timeout visuals, various small fixes and two new items --- assets/common/items/armor/back/short_0.ron | 2 +- assets/common/items/armor/back/short_1.ron | 12 ++++ assets/common/items/armor/neck/neck_1.ron | 12 ++++ .../common/items/weapons/bow/starter_bow.ron | 2 +- assets/common/loot_table.ron | 2 + assets/voxygen/element/icons/neck-0.png | 4 +- assets/voxygen/element/icons/neck-1.png | 3 + assets/voxygen/i18n/en.ron | 4 +- assets/voxygen/item_image_manifest.ron | 7 ++ assets/voxygen/voxel/armor/back/short-1.vox | 3 + .../voxel/humanoid_armor_back_manifest.ron | 6 +- common/src/comp/group.rs | 3 + server/src/events/group_manip.rs | 8 +-- server/src/state_ext.rs | 2 +- voxygen/src/hud/bag.rs | 2 +- voxygen/src/hud/group.rs | 69 +++++++------------ voxygen/src/hud/mod.rs | 5 +- voxygen/src/hud/overhead.rs | 9 ++- voxygen/src/hud/social.rs | 54 +++++++++++++-- voxygen/src/session.rs | 1 + 20 files changed, 140 insertions(+), 70 deletions(-) create mode 100644 assets/common/items/armor/back/short_1.ron create mode 100644 assets/common/items/armor/neck/neck_1.ron create mode 100644 assets/voxygen/element/icons/neck-1.png create mode 100644 assets/voxygen/voxel/armor/back/short-1.vox diff --git a/assets/common/items/armor/back/short_0.ron b/assets/common/items/armor/back/short_0.ron index 2f7f4aa223..c53d6418dd 100644 --- a/assets/common/items/armor/back/short_0.ron +++ b/assets/common/items/armor/back/short_0.ron @@ -5,7 +5,7 @@ Item( ( kind: Back("Short0"), stats: ( - protection: Normal(0.0), + protection: Normal(0.2), ), ) ), diff --git a/assets/common/items/armor/back/short_1.ron b/assets/common/items/armor/back/short_1.ron new file mode 100644 index 0000000000..800c7ca622 --- /dev/null +++ b/assets/common/items/armor/back/short_1.ron @@ -0,0 +1,12 @@ +Item( + name: "Green Blanket", + description: "Keeps your shoulders warm.", + kind: Armor( + ( + kind: Back("Short1"), + stats: ( + protection: Normal(0.1), + ), + ) + ), +) diff --git a/assets/common/items/armor/neck/neck_1.ron b/assets/common/items/armor/neck/neck_1.ron new file mode 100644 index 0000000000..8f231cc3ce --- /dev/null +++ b/assets/common/items/armor/neck/neck_1.ron @@ -0,0 +1,12 @@ +Item( + name: "Gem of lesser Protection", + description: "Surrounded by a discrete magical glow.", + kind: Armor( + ( + kind: Neck("Neck1"), + stats: ( + protection: Normal(0.5), + ), + ) + ), +) diff --git a/assets/common/items/weapons/bow/starter_bow.ron b/assets/common/items/weapons/bow/starter_bow.ron index 749283b4dc..45b7357aa7 100644 --- a/assets/common/items/weapons/bow/starter_bow.ron +++ b/assets/common/items/weapons/bow/starter_bow.ron @@ -1,6 +1,6 @@ Item( name: "Uneven Bow", - description: "Someone carved his initials into it.", + description: "Someone carved their initials into it.", kind: Tool( ( kind: Bow("ShortBow0"), diff --git a/assets/common/loot_table.ron b/assets/common/loot_table.ron index 59b741a6dc..4d0a7e98bf 100644 --- a/assets/common/loot_table.ron +++ b/assets/common/loot_table.ron @@ -230,6 +230,8 @@ (0.6, "common.items.armor.ring.ring_0"), // capes (0.6, "common.items.armor.back.short_0"), + (0.7, "common.items.armor.back.short_1"), // necks (0.6, "common.items.armor.neck.neck_0"), + (0.4, "common.items.armor.neck.neck_1"), ] diff --git a/assets/voxygen/element/icons/neck-0.png b/assets/voxygen/element/icons/neck-0.png index a7a1f6ca5e..ddd86a3dc3 100644 --- a/assets/voxygen/element/icons/neck-0.png +++ b/assets/voxygen/element/icons/neck-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:344387aec10b94e881c13b8d1ac5bb39aa085830c9b72a61f8b517c4e00684e5 -size 560 +oid sha256:efb33b5421f6ffd1dab50d382e141ca95f64a334c617263fe3de0f2895b7099e +size 555 diff --git a/assets/voxygen/element/icons/neck-1.png b/assets/voxygen/element/icons/neck-1.png new file mode 100644 index 0000000000..6da4108cb8 --- /dev/null +++ b/assets/voxygen/element/icons/neck-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53d3b5749504f4331518114b6e0f5b5b188ed8d150ab94dd2989f1388ca788ea +size 686 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index a5164163cb..e57424b780 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -336,6 +336,7 @@ magically infused items?"#, "hud.group.add_friend": "Add to Friends", "hud.group.link_group": "Link Groups", "hud.group.in_menu": "In Menu", + "hud.group.members": "Group Members", "hud.spell": "Spells", @@ -470,7 +471,8 @@ Protection "Press 'L-Shift' to open your Glider and conquer the skies.", "Veloren is still in Pre-Alpha. We do our best to improve it every day!", "If you want to join the Dev-Team or just have a chat with us join our Discord-Server.", - "You can toggle showing your amount of health on the healthbar in the settings.", + "You can toggle showing your amount of health on the healthbar in the settings.", + "In order to see your stats click the 'Stats' button in your inventory.", ], "npc.speech.villager_under_attack": [ "Help, I'm under attack!", diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index 5c71dde11b..51a2014214 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -1016,6 +1016,10 @@ Armor(Back("Short0")): VoxTrans( "voxel.armor.back.short-0", (0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.0, + ), + Armor(Back("Short1")): VoxTrans( + "voxel.armor.back.short-1", + (0.0, -2.0, 0.0), (-90.0, 180.0, 0.0), 1.0, ), Armor(Back("Admin")): VoxTrans( "voxel.armor.back.admin", @@ -1033,6 +1037,9 @@ Armor(Neck("Neck0")): Png( "element.icons.neck-0", ), + Armor(Neck("Neck1")): Png( + "element.icons.neck-1", + ), // Tabards Armor(Tabard("Admin")): Png( "element.icons.tabard_admin", diff --git a/assets/voxygen/voxel/armor/back/short-1.vox b/assets/voxygen/voxel/armor/back/short-1.vox new file mode 100644 index 0000000000..d1b1ebb8f1 --- /dev/null +++ b/assets/voxygen/voxel/armor/back/short-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1676d3a052c16e41e9cf970db19815af6a7d3ef1c68bc2792c072be42aab6f22 +size 1344 diff --git a/assets/voxygen/voxel/humanoid_armor_back_manifest.ron b/assets/voxygen/voxel/humanoid_armor_back_manifest.ron index dcda59421c..7b31c4591d 100644 --- a/assets/voxygen/voxel/humanoid_armor_back_manifest.ron +++ b/assets/voxygen/voxel/humanoid_armor_back_manifest.ron @@ -15,6 +15,10 @@ "DungPurp0": ( vox_spec: ("armor.back.dung_purp-0", (-5.0, -1.0, -14.0)), color: None - ), + ), + "Short1": ( + vox_spec: ("armor.back.short-1", (-5.0, -1.0, -11.0)), + color: None + ), }, )) diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 9c38f3d74f..84356fb599 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -145,6 +145,7 @@ impl GroupManager { // Add someone to a group // Also used to create new groups + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn add_group_member( &mut self, leader: specs::Entity, @@ -241,6 +242,7 @@ impl GroupManager { }); } + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn new_pet( &mut self, pet: specs::Entity, @@ -319,6 +321,7 @@ impl GroupManager { // Remove someone from a group if they are in one // Don't need to check if they are in a group before calling this // Also removes pets (ie call this if the pet no longer exists) + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 fn remove_from_group( &mut self, member: specs::Entity, diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index e73a617fdc..0c6777d760 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -54,7 +54,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if client.invited_to_group.is_some() { already_has_invite = true; } else { - client.notify(ServerMsg::GroupInvite((*inviter_uid).into())); + client.notify(ServerMsg::GroupInvite((*inviter_uid))); client.invited_to_group = Some(entity); } // Would be cool to do this in agent system (e.g. add an invited @@ -71,10 +71,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani .ecs() .write_storage() .insert(invitee, comp::Agent::default()); - } else { - if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); - } + } else if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); } if already_has_invite { diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index d50b5ef892..77b42d53a0 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -261,7 +261,7 @@ impl StateExt for State { | comp::ChatType::Kill | comp::ChatType::Meta | comp::ChatType::World(_) => { - self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg.clone())) + self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg)) }, comp::ChatType::Tell(u, t) => { for (client, uid) in ( diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 992232de94..e3ab174156 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -585,7 +585,7 @@ impl<'a> Widget for Bag<'a> { "{}\n\n{}\n\n{}\n\n{}%", self.stats.endurance, self.stats.fitness, self.stats.willpower, damage_reduction )) - .top_right_with_margins_on(state.ids.stats_alignment, 120.0, 150.0) + .top_right_with_margins_on(state.ids.stats_alignment, 120.0, 130.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(16)) .color(TEXT_COLOR) diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 63c70bb2dc..9da541edc3 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,15 +1,11 @@ use super::{ - img_ids::{Imgs, ImgsRot}, - Show, BLACK, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, - TEXT_COLOR_GREY, UI_HIGHLIGHT_0, + img_ids::Imgs, Show, BLACK, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, MANA_COLOR, + TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::{ - i18n::VoxygenLocalization, - settings::Settings, - ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, - window::GameInput, - GlobalState, + i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, + window::GameInput, GlobalState, }; use client::{self, Client}; use common::{ @@ -51,6 +47,8 @@ widget_ids! { member_stam[], dead_txt[], health_txt[], + timeout_bg, + timeout, } } @@ -60,7 +58,6 @@ pub struct State { selected_member: Option, } -const TOOLTIP_UPSHIFT: f64 = 40.0; #[derive(WidgetCommon)] pub struct Group<'a> { show: &'a mut Show, @@ -69,8 +66,6 @@ pub struct Group<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - tooltip_manager: &'a mut TooltipManager, - rot_imgs: &'a ImgsRot, pulse: f32, global_state: &'a GlobalState, @@ -79,6 +74,7 @@ pub struct Group<'a> { } impl<'a> Group<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn new( show: &'a mut Show, client: &'a Client, @@ -86,8 +82,6 @@ impl<'a> Group<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - tooltip_manager: &'a mut TooltipManager, - rot_imgs: &'a ImgsRot, pulse: f32, global_state: &'a GlobalState, ) -> Self { @@ -96,8 +90,6 @@ impl<'a> Group<'a> { client, settings, imgs, - rot_imgs, - tooltip_manager, fonts, localized_strings, pulse, @@ -127,37 +119,17 @@ impl<'a> Widget for Group<'a> { } } - #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } //TODO: Disband groups when there's only one member in them //TODO: Always send health, energy, level and position of group members to the // client - + #[allow(clippy::unused_unit)] // 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 button_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)) - .title_text_color(TEXT_COLOR) - .font_id(self.fonts.cyri.conrod_id) - .desc_text_color(TEXT_COLOR); // Don't show pets let group_members = self @@ -217,6 +189,20 @@ impl<'a> Widget for Group<'a> { .w_h(49.0, 26.0) .bottom_left_with_margins_on(ui.window, 190.0, 10.0) .set(state.ids.group_button, ui); + // Show timeout bar + let max_time = 90.0; + let time = 50.0; + let progress_perc = time / max_time; + Image::new(self.imgs.progress_frame) + .w_h(100.0, 10.0) + .middle_of(state.ids.bg) + .color(Some(UI_MAIN)) + .set(state.ids.timeout_bg, ui); + Image::new(self.imgs.progress) + .w_h(98.0 * progress_perc, 8.0) + .top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.timeout, ui); } // Buttons if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) { @@ -230,13 +216,6 @@ impl<'a> Widget for Group<'a> { .bottom_left_with_margins_on(ui.window, 190.0, 10.0) .hover_image(self.imgs.group_icon_hover) .press_image(self.imgs.group_icon_press) - .with_tooltip( - self.tooltip_manager, - &localized_strings.get("hud.group"), - "", - &button_tooltip, - ) - .bottom_offset(TOOLTIP_UPSHIFT) .set(state.ids.group_button, ui) .was_clicked() { @@ -319,7 +298,7 @@ impl<'a> Widget for Group<'a> { // change panel positions when debug info is shown let offset = if self.global_state.settings.gameplay.toggle_debug { - 210.0 + 240.0 } else { 110.0 }; diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index fdb13ab68a..33d400735e 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1933,8 +1933,9 @@ impl Hud { &self.imgs, &self.fonts, &self.voxygen_i18n, - //&stats, info.selected_entity, + &self.rot_imgs, + tooltip_manager, ) .set(self.ids.social_window, ui_widgets) { @@ -1959,8 +1960,6 @@ impl Hud { &self.imgs, &self.fonts, &self.voxygen_i18n, - tooltip_manager, - &self.rot_imgs, self.pulse, &global_state, ) diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index f3079b1630..b14f39c213 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -148,12 +148,15 @@ impl<'a> Widget for Overhead<'a> { const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0; let hp_percentage = self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; - let name_y = if hp_percentage == 100.0 { - MANA_BAR_Y + 10.0 + let level_comp = self.stats.level.level() as i64 - self.own_level as i64; + let name_y = if hp_percentage.abs() > 99.9 { + MANA_BAR_Y + 20.0 + } else if level_comp > 9 { + MANA_BAR_Y + 38.0 } else { MANA_BAR_Y + 32.0 }; - let font_size = if hp_percentage == 100.0 { 24 } else { 20 }; + let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 }; // Name Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index ee875b26e5..ceb8f94674 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -1,6 +1,12 @@ -use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN}; +use super::{ + img_ids::{Imgs, ImgsRot}, + Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN, +}; -use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use crate::{ + i18n::VoxygenLocalization, + ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, +}; use client::{self, Client}; use common::sync::Uid; use conrod_core::{ @@ -63,30 +69,34 @@ pub struct Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - //stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, + rot_imgs: &'a ImgsRot, + tooltip_manager: &'a mut TooltipManager, #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Social<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn new( show: &'a Show, client: &'a Client, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, - //stats: &'a Stats, selected_entity: Option<(specs::Entity, Instant)>, + rot_imgs: &'a ImgsRot, + tooltip_manager: &'a mut TooltipManager, ) -> Self { Self { show, client, imgs, + rot_imgs, fonts, localized_strings, - //stats, + tooltip_manager, selected_entity, common: widget::CommonBuilder::default(), } @@ -118,6 +128,24 @@ impl<'a> Widget for Social<'a> { let widget::UpdateArgs { state, ui, .. } = args; let mut events = Vec::new(); + let button_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)) + .title_text_color(TEXT_COLOR) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); // Window frame and BG let pos = if self.show.group || self.show.group_menu { @@ -472,7 +500,20 @@ impl<'a> Widget for Social<'a> { self.selected_entity .and_then(|s| self.client.state().read_component_copied(s.0)) }); - + // TODO: Prevent inviting players with the same group uid + // TODO: Show current amount of group members as a tooltip for the invite button + // if the player is the group leader TODO: Grey out the invite + // button if the group has 6/6 members + let current_members = 4; + let tooltip_txt = if selected_ingame.is_some() { + format!( + "{}/6 {}", + ¤t_members, + &self.localized_strings.get("hud.group.members") + ) + } else { + (&self.localized_strings.get("hud.group.members")).to_string() + }; if Button::image(self.imgs.button) .w_h(106.0, 26.0) .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0) @@ -500,6 +541,7 @@ impl<'a> Widget for Social<'a> { }) .label_font_size(self.fonts.cyri.scale(15)) .label_font_id(self.fonts.cyri.conrod_id) + .with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip) .set(state.ids.invite_button, ui) .was_clicked() { diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index e4e47c8c8e..3c5f266164 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1071,6 +1071,7 @@ impl PlayState for SessionState { /// Max distance an entity can be "targeted" const MAX_TARGET_RANGE: f32 = 30.0; /// Calculate what the cursor is pointing at within the 3d scene +#[allow(clippy::type_complexity)] fn under_cursor( client: &Client, cam_pos: Vec3, From 14b0d9a7feafc0acfeaeedf3c35dec2ec13095d5 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Wed, 5 Aug 2020 13:29:42 +0200 Subject: [PATCH 22/71] group leader colouring, offset fix when debug menu is open, fixed social tab visuals text width limit group interaction wording, group window adjustments --- .../element/misc_bg/social_tab_online.png | 3 + assets/voxygen/i18n/de_DE.ron | 14 ++- server/src/events/group_manip.rs | 47 ++++---- voxygen/src/hud/group.rs | 102 ++++++++++-------- voxygen/src/hud/img_ids.rs | 1 + voxygen/src/hud/social.rs | 19 +--- 6 files changed, 99 insertions(+), 87 deletions(-) create mode 100644 assets/voxygen/element/misc_bg/social_tab_online.png diff --git a/assets/voxygen/element/misc_bg/social_tab_online.png b/assets/voxygen/element/misc_bg/social_tab_online.png new file mode 100644 index 0000000000..f4f6f41589 --- /dev/null +++ b/assets/voxygen/element/misc_bg/social_tab_online.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d40285966edd6360907725a309a7bc090667c3685cb7f8cb734e41aa06ea15d +size 644 diff --git a/assets/voxygen/i18n/de_DE.ron b/assets/voxygen/i18n/de_DE.ron index a0aa8387b7..11c6caabb0 100644 --- a/assets/voxygen/i18n/de_DE.ron +++ b/assets/voxygen/i18n/de_DE.ron @@ -69,6 +69,7 @@ VoxygenLocalization( "common.error": "Fehler", "common.fatal_error": "Fataler Fehler", "common.decline": "Ablehnen", + "common.you": "Ihr", /// End Common section // Message when connection to the server is lost @@ -321,15 +322,17 @@ magischen Gegenstände ergattern?"#, "hud.social.zone" : "Gebiet", "hud.group": "Gruppe", - "hud.group.invite": "Einladen", - "hud.group.leave": "Verlassen", - "hud.group.add_friend": "Freund hinzufügen", + "hud.group.invite_to_join": "{name} lädt euch in seine Gruppe ein!", + "hud.group.invite": "Einladen", + "hud.group.kick": "Kicken", "hud.group.assign_leader": "Anführer", - "hud.group.kick": "Entfernen", - "hud.group.invite_to_join": "{name} hat euch zu seiner Gruppe eingeladen!", + "hud.group.leave": "Gruppe Verlassen", "hud.group.dead" : "Tot", "hud.group.out_of_range": "Außer Reichweite", + "hud.group.add_friend": "Freund hinzufügen", "hud.group.link_group": "Gruppen verbinden", + "hud.group.in_menu": "In Menü", + "hud.group.members": "Gruppen Mitglieder", "hud.crafting": "Herstellen", "hud.crafting.recipes": "Rezepte", @@ -394,6 +397,7 @@ magischen Gegenstände ergattern?"#, "gameinput.dance": "Tanzen", "gameinput.declinegroupinvite": "Ablehnen", "gameinput.acceptgroupinvite": "Annehmen", + "gameinput.select": "Auswählen", /// End GameInput section diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index 0c6777d760..1479823d43 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -26,7 +26,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if let Some(client) = clients.get_mut(entity) { client.notify( ChatType::Meta - .server_msg("Invite failed, target does not exist".to_owned()), + .server_msg("Invite failed, target does not exist.".to_owned()), ); } return; @@ -54,7 +54,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if client.invited_to_group.is_some() { already_has_invite = true; } else { - client.notify(ServerMsg::GroupInvite((*inviter_uid))); + client.notify(ServerMsg::GroupInvite(*inviter_uid)); client.invited_to_group = Some(entity); } // Would be cool to do this in agent system (e.g. add an invited @@ -72,15 +72,16 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani .write_storage() .insert(invitee, comp::Agent::default()); } else if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + client.notify(ChatType::Meta.server_msg("Invite rejected.".to_owned())); } if already_has_invite { // Inform inviter that there is already an invite if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg( - "Invite failed target already has a pending invite".to_owned(), - )); + client.notify( + ChatType::Meta + .server_msg("This player already has a pending invite.".to_owned()), + ); } } @@ -143,7 +144,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani // Inform inviter of rejection if let Some(client) = clients.get_mut(inviter) { // TODO: say who declined the invite - client.notify(ChatType::Meta.server_msg("Invite declined".to_owned())); + client.notify(ChatType::Meta.server_msg("Invite declined.".to_owned())); } } }, @@ -181,7 +182,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if let Some(client) = clients.get_mut(entity) { client.notify( ChatType::Meta - .server_msg("Kick failed, target does not exist".to_owned()), + .server_msg("Kick failed, target does not exist.".to_owned()), ); } return; @@ -193,7 +194,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani { if let Some(client) = clients.get_mut(entity) { client.notify( - ChatType::Meta.server_msg("Kick failed, can't kick pet".to_owned()), + ChatType::Meta.server_msg("Kick failed, you can't kick pets.".to_owned()), ); } return; @@ -202,7 +203,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if uids.get(entity).map_or(false, |u| *u == uid) { if let Some(client) = clients.get_mut(entity) { client.notify( - ChatType::Meta.server_msg("Kick failed, can't kick yourself".to_owned()), + ChatType::Meta + .server_msg("Kick failed, you can't kick yourself.".to_owned()), ); } return; @@ -238,19 +240,20 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani // Tell them the have been kicked if let Some(client) = clients.get_mut(target) { client.notify( - ChatType::Meta.server_msg("The group leader kicked you".to_owned()), + ChatType::Meta + .server_msg("You were removed from the group.".to_owned()), ); } // Tell kicker that they were succesful if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg("Kick complete".to_owned())); + client.notify(ChatType::Meta.server_msg("Player kicked.".to_owned())); } }, Some(_) => { // Inform kicker that they are not the leader if let Some(client) = clients.get_mut(entity) { client.notify(ChatType::Meta.server_msg( - "Kick failed: you are not the leader of the target's group".to_owned(), + "Kick failed: You are not the leader of the target's group.".to_owned(), )); } }, @@ -259,7 +262,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if let Some(client) = clients.get_mut(entity) { client.notify( ChatType::Meta.server_msg( - "Kick failed: your target is not in a group".to_owned(), + "Kick failed: Your target is not in a group.".to_owned(), ), ); } @@ -309,14 +312,16 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ); // Tell them they are the leader if let Some(client) = clients.get_mut(target) { - client.notify(ChatType::Meta.server_msg( - "The group leader has passed leadership to you".to_owned(), - )); + client.notify( + ChatType::Meta.server_msg("You are the group leader now.".to_owned()), + ); } // Tell the old leader that the transfer was succesful if let Some(client) = clients.get_mut(target) { - client - .notify(ChatType::Meta.server_msg("Leadership transferred".to_owned())); + client.notify( + ChatType::Meta + .server_msg("You are no longer the group leader.".to_owned()), + ); } }, Some(_) => { @@ -324,7 +329,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani if let Some(client) = clients.get_mut(entity) { client.notify( ChatType::Meta.server_msg( - "Transfer failed: you are not the leader of the target's group" + "Transfer failed: You are not the leader of the target's group." .to_owned(), ), ); @@ -334,7 +339,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani // Inform transferer that the target is not in a group if let Some(client) = clients.get_mut(entity) { client.notify(ChatType::Meta.server_msg( - "Transfer failed: your target is not in a group".to_owned(), + "Transfer failed: Your target is not in a group.".to_owned(), )); } }, diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 9da541edc3..a35a37db83 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -1,6 +1,6 @@ use super::{ - img_ids::Imgs, Show, BLACK, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, MANA_COLOR, - TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, + 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, }; use crate::{ @@ -25,7 +25,7 @@ widget_ids! { group_button, bg, title, - close, + title_bg, btn_bg, btn_friend, btn_leader, @@ -35,7 +35,6 @@ widget_ids! { scroll_area, scrollbar, members[], - invite_bubble, bubble_frame, btn_accept, btn_decline, @@ -178,8 +177,8 @@ impl<'a> Widget for Group<'a> { // broken if self.show.group_menu || open_invite.is_some() { // Frame - Rectangle::fill_with([220.0, 165.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) - .bottom_left_with_margins_on(ui.window, 220.0, 10.0) + Rectangle::fill_with([220.0, 140.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) + .bottom_left_with_margins_on(ui.window, 108.0, 490.0) .crop_kids() .set(state.ids.bg, ui); } @@ -187,7 +186,7 @@ impl<'a> Widget for Group<'a> { // Group Menu button Button::image(self.imgs.group_icon) .w_h(49.0, 26.0) - .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .bottom_left_with_margins_on(ui.window, 10.0, 490.0) .set(state.ids.group_button, ui); // Show timeout bar let max_time = 90.0; @@ -213,7 +212,7 @@ impl<'a> Widget for Group<'a> { self.imgs.group_icon }) .w_h(49.0, 26.0) - .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .bottom_left_with_margins_on(ui.window, 10.0, 490.0) .hover_image(self.imgs.group_icon_hover) .press_image(self.imgs.group_icon_press) .set(state.ids.group_button, ui) @@ -221,6 +220,18 @@ impl<'a> Widget for Group<'a> { { self.show.group_menu = !self.show.group_menu; }; + Text::new(&group_name) + .up_from(state.ids.group_button, 5.0) + .font_size(14) + .font_id(self.fonts.cyri.conrod_id) + .color(BLACK) + .set(state.ids.title_bg, ui); + Text::new(&group_name) + .bottom_right_with_margins_on(state.ids.title_bg, 1.0, 1.0) + .font_size(14) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); // Member panels let group_size = group_members.len(); if state.ids.member_panels_bg.len() < group_size { @@ -298,7 +309,7 @@ impl<'a> Widget for Group<'a> { // change panel positions when debug info is shown let offset = if self.global_state.settings.gameplay.toggle_debug { - 240.0 + 290.0 } else { 110.0 }; @@ -316,10 +327,15 @@ impl<'a> Widget for Group<'a> { 21..=40 => LOW_HP_COLOR, _ => HP_COLOR, }; + let lead = if uid == leader { true } else { false }; // Don't show panel for the player! // Panel BG back.w_h(152.0, 36.0) - .color(Some(TEXT_COLOR)) + .color(if lead { + Some(ERROR_COLOR) + } else { + Some(TEXT_COLOR) + }) .set(state.ids.member_panels_bg[i], ui); // Health Image::new(self.imgs.bar_content) @@ -375,12 +391,14 @@ impl<'a> Widget for Group<'a> { .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(GROUP_COLOR) + .color(if lead { 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; @@ -434,19 +452,13 @@ impl<'a> Widget for Group<'a> { if self.show.group_menu { let selected = state.selected_member; - Text::new(&group_name) - .mid_top_with_margin_on(state.ids.bg, 2.0) - .font_size(20) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.title, ui); - if Button::image(self.imgs.button) + if Button::image(self.imgs.button) // Change button behaviour and style when the friendslist is working .w_h(90.0, 22.0) - .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) - .hover_image(self.imgs.button) // Change this when the friendslist is working - .press_image(self.imgs.button) // Change this when the friendslist is working - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working - .image_color (TEXT_COLOR_GREY) // Change this when the friendslist is working + .top_right_with_margins_on(state.ids.bg, 5.0, 5.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label_color(TEXT_COLOR_GREY) + .image_color(TEXT_COLOR_GREY) .label(&self.localized_strings.get("hud.group.add_friend")) .label_font_id(self.fonts.cyri.conrod_id) .label_font_size(self.fonts.cyri.scale(10)) @@ -495,19 +507,19 @@ impl<'a> Widget for Group<'a> { } }; if Button::image(self.imgs.button) - .w_h(90.0, 22.0) - .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) - .hover_image(self.imgs.button) - .press_image(self.imgs.button) - .label(&self.localized_strings.get("hud.group.link_group")) - .hover_image(self.imgs.button) // Change this when the friendslist is working - .press_image(self.imgs.button) // Change this when the friendslist is working - .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working - .image_color (TEXT_COLOR_GREY) // Change this when the friendslist is working - .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(10)) - .set(state.ids.btn_link, ui) - .was_clicked() + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label(&self.localized_strings.get("hud.group.link_group")) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label_color(TEXT_COLOR_GREY) + .image_color(TEXT_COLOR_GREY) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_link, ui) + .was_clicked() {}; if Button::image(self.imgs.button) .w_h(90.0, 22.0) @@ -546,8 +558,8 @@ impl<'a> Widget for Group<'a> { } // Scrollable area for group member names Rectangle::fill_with([110.0, 135.0], color::TRANSPARENT) - .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) - .scroll_kids() + .top_left_with_margins_on(state.ids.bg, 5.0, 5.0) + .crop_kids() .scroll_kids_vertically() .set(state.ids.scroll_area, ui); Scrollbar::y_axis(state.ids.scroll_area) @@ -558,7 +570,6 @@ impl<'a> Widget for Group<'a> { for (i, &uid) in group_members.iter().copied().enumerate() { let selected = state.selected_member.map_or(false, |u| u == uid); let char_name = uid_to_name_text(uid, &self.client); - // TODO: Do something special visually if uid == leader if Button::image(if selected { self.imgs.selection @@ -578,7 +589,11 @@ impl<'a> Widget for Group<'a> { .crop_kids() .label_x(Relative::Place(Place::Start(Some(4.0)))) .label(&char_name) - .label_color(TEXT_COLOR) + .label_color(if uid == leader { + ERROR_COLOR + } else { + TEXT_COLOR + }) .label_font_id(self.fonts.cyri.conrod_id) .label_font_size(self.fonts.cyri.scale(12)) .set(state.ids.members[i], ui) @@ -609,10 +624,11 @@ impl<'a> Widget for Group<'a> { .get("hud.group.invite_to_join") .replace("{name}", &name); Text::new(&invite_text) - .mid_top_with_margin_on(state.ids.bg, 20.0) + .mid_top_with_margin_on(state.ids.bg, 5.0) .font_size(12) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) + .w(165.0) // Text stays within frame .set(state.ids.title, ui); // Accept Button let accept_key = self @@ -632,7 +648,7 @@ impl<'a> Widget for Group<'a> { )) .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) + .label_font_size(self.fonts.cyri.scale(12)) .set(state.ids.btn_accept, ui) .was_clicked() { @@ -657,7 +673,7 @@ impl<'a> Widget for Group<'a> { )) .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) - .label_font_size(self.fonts.cyri.scale(15)) + .label_font_size(self.fonts.cyri.scale(12)) .set(state.ids.btn_decline, ui) .was_clicked() { diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 79eda06010..04f60f0b3a 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -66,6 +66,7 @@ image_ids! { social_frame_fact: "voxygen.element.misc_bg.social_frame", social_bg_fact: "voxygen.element.misc_bg.social_bg", social_tab_act: "voxygen.element.buttons.social_tab_active", + social_tab_online: "voxygen.element.misc_bg.social_tab_online", social_tab_inact: "voxygen.element.buttons.social_tab_inactive", social_tab_inact_hover: "voxygen.element.buttons.social_tab_inactive", social_tab_inact_press: "voxygen.element.buttons.social_tab_inactive", diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index ceb8f94674..e1158019a4 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -27,7 +27,6 @@ widget_ids! { scrollbar, online_align, online_tab, - online_tab_icon, names_align, name_txt, player_levels[], @@ -205,18 +204,10 @@ impl<'a> Widget for Social<'a> { // Tabs Buttons // Online Tab Button if Button::image(match &self.show.social_tab { - SocialTab::Online => self.imgs.social_tab_act, + SocialTab::Online => self.imgs.social_tab_online, _ => self.imgs.social_tab_inact, }) .w_h(30.0, 44.0) - .hover_image(match &self.show.social_tab { - SocialTab::Online => self.imgs.social_tab_act, - _ => self.imgs.social_tab_inact_hover, - }) - .press_image(match &self.show.social_tab { - SocialTab::Online => self.imgs.social_tab_act, - _ => self.imgs.social_tab_inact_press, - }) .image_color(match &self.show.social_tab { SocialTab::Online => UI_MAIN, _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), @@ -227,14 +218,6 @@ impl<'a> Widget for Social<'a> { { events.push(Event::ChangeSocialTab(SocialTab::Online)); } - Image::new(self.imgs.chat_online_small) - .w_h(20.0, 20.0) - .top_right_with_margins_on(state.ids.online_tab, 12.0, 7.0) - .color(match &self.show.social_tab { - SocialTab::Online => Some(TEXT_COLOR), - _ => Some(UI_MAIN), - }) - .set(state.ids.online_tab_icon, ui); // Friends Tab Button if Button::image(match &self.show.social_tab { SocialTab::Friends => self.imgs.social_tab_act, From def68302c786b12a4d11bae84a5c7367c0fce24a Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Wed, 5 Aug 2020 19:50:12 +0200 Subject: [PATCH 23/71] Fixed test world to run again Update test_world.rs --- server/src/test_world.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/server/src/test_world.rs b/server/src/test_world.rs index 1b6d070e61..b95fabfb61 100644 --- a/server/src/test_world.rs +++ b/server/src/test_world.rs @@ -1,5 +1,5 @@ use common::{ - generation::{ChunkSupplement, EntityInfo, EntityKind}, + generation::{ChunkSupplement, EntityInfo}, terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, vol::{ReadVol, RectVolSize, Vox, WriteVol}, }; @@ -30,17 +30,10 @@ impl World { let mut supplement = ChunkSupplement::default(); - if chunk_pos.map(|e| e % 8 == 0).reduce_and() { - supplement = supplement.with_entity(EntityInfo { - pos: Vec3::::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0, - kind: EntityKind::Waypoint, - }); - } - Ok(( TerrainChunk::new( 256 + if rng.gen::() < 64 { height } else { 0 }, - Block::new(BlockKind::Dense, Rgb::new(200, 220, 255)), + Block::new(BlockKind::Grass, Rgb::new(11, 102, 35)), Block::empty(), TerrainChunkMeta::void(), ), From 390d289d35b60cfc1421e56a3e4803076810578e Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 6 Aug 2020 21:59:28 -0400 Subject: [PATCH 24/71] Add timeout's to group invites, and configurable limit to group size Fix a few group bugs, enable invite timeout and group limits in ui --- .cargo/config | 4 +- Cargo.lock | 32 +---- client/src/lib.rs | 50 ++++++- common/src/comp/group.rs | 66 ++++++--- common/src/msg/mod.rs | 2 +- common/src/msg/server.rs | 24 +++- common/src/state.rs | 2 + common/src/sys/agent.rs | 26 +++- server/src/client.rs | 1 - server/src/events/group_manip.rs | 228 ++++++++++++++++++++++--------- server/src/events/player.rs | 2 +- server/src/lib.rs | 12 +- server/src/settings.rs | 2 + server/src/sys/invite_timeout.rs | 71 ++++++++++ server/src/sys/mod.rs | 4 + voxygen/src/hud/group.rs | 11 +- voxygen/src/hud/social.rs | 102 ++++++++------ 17 files changed, 467 insertions(+), 172 deletions(-) create mode 100644 server/src/sys/invite_timeout.rs diff --git a/.cargo/config b/.cargo/config index 6a9b75f52a..e4f3071f5c 100644 --- a/.cargo/config +++ b/.cargo/config @@ -4,4 +4,6 @@ rustflags = [ ] [alias] -generate = "run --package tools --" \ No newline at end of file +generate = "run --package tools --" +test-server = "run --bin veloren-server-cli --no-default-features" + diff --git a/Cargo.lock b/Cargo.lock index bb5d2f2430..709ab9ea4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,16 +219,6 @@ dependencies = [ "winapi 0.3.8", ] -[[package]] -name = "auth-common" -version = "0.1.0" -source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086" -dependencies = [ - "rand 0.7.3", - "serde", - "uuid", -] - [[package]] name = "auth-common" version = "0.1.0" @@ -239,26 +229,12 @@ dependencies = [ "uuid", ] -[[package]] -name = "authc" -version = "1.0.0" -source = "git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086#223a4097f7ebc8d451936dccb5e6517194bbf086" -dependencies = [ - "auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)", - "fxhash", - "hex", - "rust-argon2 0.8.2", - "serde_json", - "ureq", - "uuid", -] - [[package]] name = "authc" version = "1.0.0" source = "git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe#b943c85e4a38f5ec60cd18c34c73097640162bfe" dependencies = [ - "auth-common 0.1.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", + "auth-common", "fxhash", "hex", "rust-argon2 0.8.2", @@ -4625,7 +4601,7 @@ dependencies = [ name = "veloren-client" version = "0.6.0" dependencies = [ - "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", + "authc", "byteorder 1.3.4", "futures-executor", "futures-timer", @@ -4646,7 +4622,7 @@ name = "veloren-common" version = "0.6.0" dependencies = [ "arraygen", - "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=223a4097f7ebc8d451936dccb5e6517194bbf086)", + "authc", "criterion", "crossbeam", "dot_vox", @@ -4674,7 +4650,7 @@ dependencies = [ name = "veloren-server" version = "0.6.0" dependencies = [ - "authc 1.0.0 (git+https://gitlab.com/veloren/auth.git?rev=b943c85e4a38f5ec60cd18c34c73097640162bfe)", + "authc", "chrono", "crossbeam", "diesel", diff --git a/client/src/lib.rs b/client/src/lib.rs index ee8d99224f..aec0cb919c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,7 +23,7 @@ use common::{ msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, - MAX_BYTES_CHAT_MSG, + MAX_BYTES_CHAT_MSG, InviteAnswer, }, recipe::RecipeBook, state::State, @@ -79,9 +79,14 @@ pub struct Client { recipe_book: RecipeBook, available_recipes: HashSet, - group_invite: Option, + max_group_size: u32, + // Client has received an invite (inviter uid, time out instant) + group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>, group_leader: Option, + // Note: potentially representable as a client only component group_members: HashMap, + // Pending invites that this client has sent out + pending_invites: HashSet, _network: Network, participant: Option, @@ -130,13 +135,14 @@ impl Client { let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?; // Wait for initial sync - let (state, entity, server_info, world_map, recipe_book) = block_on(async { + let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(async { loop { match stream.recv().await? { ServerMsg::InitialSync { entity_package, server_info, time_of_day, + max_group_size, world_map: (map_size, world_map), recipe_book, } => { @@ -188,6 +194,7 @@ impl Client { server_info, (world_map, map_size), recipe_book, + max_group_size, )); }, ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), @@ -212,14 +219,16 @@ impl Client { server_info, world_map, player_list: HashMap::new(), - group_members: HashMap::new(), character_list: CharacterList::default(), active_character_id: None, recipe_book, available_recipes: HashSet::default(), + max_group_size, group_invite: None, group_leader: None, + group_members: HashMap::new(), + pending_invites: HashSet::new(), _network: network, participant: Some(participant), @@ -432,7 +441,9 @@ impl Client { .unwrap(); } - pub fn group_invite(&self) -> Option { self.group_invite } + pub fn max_group_size(&self) -> u32 { self.max_group_size } + + pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { self.group_invite } pub fn group_info(&self) -> Option<(String, Uid)> { self.group_leader.map(|l| ("Group".into(), l)) // TODO @@ -440,6 +451,8 @@ impl Client { pub fn group_members(&self) -> &HashMap { &self.group_members } + pub fn pending_invites(&self) -> &HashSet { &self.pending_invites } + pub fn send_group_invite(&mut self, invitee: Uid) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( @@ -758,6 +771,10 @@ impl Client { frontend_events.append(&mut self.handle_new_messages()?); // 3) Update client local data + // Check if the group invite has timed out and remove if so + if self.group_invite.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) { + self.group_invite = None; + } // 4) Tick the client's LocalState self.state.tick(dt, add_foreign_systems, true); @@ -1046,9 +1063,28 @@ impl Client { }, } }, - ServerMsg::GroupInvite(uid) => { - self.group_invite = Some(uid); + ServerMsg::GroupInvite { inviter, timeout } => { + self.group_invite = Some((inviter, std::time::Instant::now(), timeout)); }, + ServerMsg::InvitePending(uid) => { + if !self.pending_invites.insert(uid) { + warn!("Received message about pending invite that was already pending"); + } + } + ServerMsg::InviteComplete { target, answer } => { + if !self.pending_invites.remove(&target) { + warn!("Received completed invite message for invite that was not in the list of pending invites") + } + // TODO: expose this as a new event variant instead of going + // through the chat + let msg = match answer { + // TODO: say who accepted/declined/timed out the invite + InviteAnswer::Accepted => "Invite accepted", + InviteAnswer::Declined => "Invite declined", + InviteAnswer::TimedOut => "Invite timed out", + }; + frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg))); + } ServerMsg::Ping => { self.singleton_stream.send(ClientMsg::Pong)?; }, diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 84356fb599..18de30ed8b 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -9,9 +9,11 @@ use tracing::{error, warn}; // Primitive group system // Shortcomings include: // - no support for more complex group structures -// - lack of complex enemy npc integration +// - lack of npc group integration // - relies on careful management of groups to maintain a valid state // - the possesion rod could probably wreck this +// - clients don't know which pets are theirs (could be easy to solve by +// putting owner uid in Role::Pet) #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Group(u32); @@ -26,11 +28,26 @@ impl Component for Group { type Storage = FlaggedStorage>; } +pub struct Invite(pub specs::Entity); +impl Component for Invite { + type Storage = IdvStorage; +} + +// Pending invites that an entity currently has sent out +// (invited entity, instant when invite times out) +pub struct PendingInvites(pub Vec<(specs::Entity, std::time::Instant)>); +impl Component for PendingInvites { + type Storage = IdvStorage; +} + #[derive(Clone, Debug)] pub struct GroupInfo { // TODO: what about enemy groups, either the leader will constantly change because they have to // be loaded or we create a dummy entity or this needs to be optional pub leader: specs::Entity, + // Number of group members (excluding pets) + pub num_members: u32, + // Name of the group pub name: String, } @@ -134,9 +151,14 @@ impl GroupManager { self.groups.get(group.0 as usize) } - fn create_group(&mut self, leader: specs::Entity) -> Group { + fn group_info_mut(&mut self, group: Group) -> Option<&mut GroupInfo> { + self.groups.get_mut(group.0 as usize) + } + + fn create_group(&mut self, leader: specs::Entity, num_members: u32) -> Group { Group(self.groups.insert(GroupInfo { leader, + num_members, name: "Group".into(), }) as u32) } @@ -204,15 +226,20 @@ impl GroupManager { None => None, }; - let group = group.unwrap_or_else(|| { - let new_group = self.create_group(leader); + let group = if let Some(group) = group { + // Increment group size + // Note: unwrap won't fail since we just retrieved the group successfully above + self.group_info_mut(group).unwrap().num_members += 1; + group + } else { + let new_group = self.create_group(leader, 2); // Unwrap should not fail since we just found these entities and they should // still exist Note: if there is an issue replace with a warn groups.insert(leader, new_group).unwrap(); // Inform notifier(leader, ChangeNotification::NewLeader(leader)); new_group - }); + }; let new_pets = pets(new_member, new_member_uid, alignments, entities); @@ -256,7 +283,7 @@ impl GroupManager { let group = match groups.get(owner).copied() { Some(group) => group, None => { - let new_group = self.create_group(owner); + let new_group = self.create_group(owner, 1); groups.insert(owner, new_group).unwrap(); // Inform notifier(owner, ChangeNotification::NewLeader(owner)); @@ -348,7 +375,7 @@ impl GroupManager { (entities, uids, &*groups, alignments.maybe()) .join() - .filter(|(e, _, g, _)| **g == group && (!to_be_deleted || *e == member)) + .filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member)) .fold( HashMap::, Vec)>::new(), |mut acc, (e, uid, _, alignment)| { @@ -356,9 +383,11 @@ impl GroupManager { Alignment::Owned(owner) if uid != owner => Some(owner), _ => None, }) { + // A pet // Assumes owner will be in the group acc.entry(*owner).or_default().1.push(e); } else { + // Not a pet acc.entry(*uid).or_default().0 = Some(e); } @@ -375,7 +404,7 @@ impl GroupManager { members.push((owner, Role::Member)); // New group - let new_group = self.create_group(owner); + let new_group = self.create_group(owner, 1); for (member, _) in &members { groups.insert(*member, new_group).unwrap(); } @@ -401,7 +430,7 @@ impl GroupManager { let leaving_member_uid = if let Some(uid) = uids.get(member) { *uid } else { - error!("Failed to retrieve uid for the new group member"); + error!("Failed to retrieve uid for the leaving member"); return; }; @@ -409,7 +438,7 @@ impl GroupManager { // If pets and not about to be deleted form new group if !leaving_pets.is_empty() && !to_be_deleted { - let new_group = self.create_group(member); + let new_group = self.create_group(member, 1); notifier(member, ChangeNotification::NewGroup { leader: member, @@ -432,12 +461,11 @@ impl GroupManager { }); } - if let Some(info) = self.group_info(group) { + if let Some(info) = self.group_info_mut(group) { + info.num_members -= 1; // Inform remaining members - let mut num_members = 0; - members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| { - num_members += 1; - match role { + members(group, &*groups, entities, alignments, uids).for_each( + |(e, role)| match role { Role::Member => { notifier(e, ChangeNotification::Removed(member)); leaving_pets.iter().for_each(|p| { @@ -445,16 +473,16 @@ impl GroupManager { }) }, Role::Pet => {}, - } - }); + }, + ); // If leader is the last one left then disband the group // Assumes last member is the leader - if num_members == 1 { + if info.num_members == 1 { let leader = info.leader; self.remove_group(group); groups.remove(leader); notifier(leader, ChangeNotification::NoGroup); - } else if num_members == 0 { + } else if info.num_members == 0 { error!("Somehow group has no members") } } diff --git a/common/src/msg/mod.rs b/common/src/msg/mod.rs index f7d8c3d118..4f2f1585b1 100644 --- a/common/src/msg/mod.rs +++ b/common/src/msg/mod.rs @@ -7,7 +7,7 @@ pub use self::{ client::ClientMsg, ecs_packet::EcsCompPacket, server::{ - CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError, + CharacterInfo, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, }, }; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 4c3fda2aab..156c18fd61 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -47,6 +47,13 @@ pub struct CharacterInfo { pub level: u32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum InviteAnswer { + Accepted, + Declined, + TimedOut, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Notification { WaypointSaved, @@ -59,6 +66,7 @@ pub enum ServerMsg { entity_package: sync::EntityPackage, server_info: ServerInfo, time_of_day: state::TimeOfDay, + max_group_size: u32, world_map: (Vec2, Vec), recipe_book: RecipeBook, }, @@ -70,7 +78,21 @@ pub enum ServerMsg { CharacterActionError(String), PlayerListUpdate(PlayerListUpdate), GroupUpdate(comp::group::ChangeNotification), - GroupInvite(sync::Uid), + // Indicate to the client that they are invited to join a group + GroupInvite { + inviter: sync::Uid, + timeout: std::time::Duration, + }, + // Indicate to the client that their sent invite was not invalid and is currently pending + InvitePending(sync::Uid), + // Note: this could potentially include all the failure cases such as inviting yourself in + // which case the `InvitePending` message could be removed and the client could consider their + // invite pending until they receive this message + // Indicate to the client the result of their invite + InviteComplete { + target: sync::Uid, + answer: InviteAnswer, + }, StateAnswer(Result), /// Trigger cleanup for when the client goes back to the `Registered` state /// from an ingame state diff --git a/common/src/state.rs b/common/src/state.rs index a76698c944..3d003b7a4f 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -158,6 +158,8 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 51e31a7f14..e2cb88b750 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -3,9 +3,11 @@ use crate::{ self, agent::Activity, group, + group::Invite, item::{tool::ToolKind, ItemKind}, - Agent, Alignment, Body, CharacterState, ControlAction, Controller, Loadout, MountState, - Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, Vel, + Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, + GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, + Vel, }, event::{EventBus, ServerEvent}, path::{Chaser, TraversalConfig}, @@ -49,6 +51,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, ReadStorage<'a, MountState>, + ReadStorage<'a, Invite>, ); #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -77,6 +80,7 @@ impl<'a> System<'a> for Sys { mut agents, mut controllers, mount_states, + invites, ): Self::SystemData, ) { for ( @@ -494,5 +498,23 @@ impl<'a> System<'a> for Sys { debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); } + + // Proccess group invites + for (_invite, alignment, agent, controller) in + (&invites, &alignments, &mut agents, &mut controllers).join() + { + let accept = matches!(alignment, Alignment::Npc); + if accept { + // Clear agent comp + *agent = Agent::default(); + controller + .events + .push(ControlEvent::GroupManip(GroupManip::Accept)); + } else { + controller + .events + .push(ControlEvent::GroupManip(GroupManip::Decline)); + } + } } } diff --git a/server/src/client.rs b/server/src/client.rs index 5c14688ae2..95e6d96b91 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -18,7 +18,6 @@ pub struct Client { pub network_error: AtomicBool, pub last_ping: f64, pub login_msg_sent: bool, - pub invited_to_group: Option, } impl Component for Client { diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index 1479823d43..d1a030ce1c 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -2,18 +2,25 @@ use crate::{client::Client, Server}; use common::{ comp::{ self, - group::{self, GroupManager}, + group::{self, Group, GroupManager, Invite, PendingInvites}, ChatType, GroupManip, }, - msg::ServerMsg, + msg::{InviteAnswer, ServerMsg}, sync, sync::WorldSyncExt, }; use specs::world::WorldExt; -use tracing::warn; +use std::time::{Duration, Instant}; +use tracing::{error, warn}; + +/// Time before invite times out +const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31); +/// Reduced duration shown to the client to help alleviate latency issues +const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30); // TODO: turn chat messages into enums pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { + let max_group_size = server.settings().max_player_group_size; let state = server.state_mut(); match manip { @@ -44,38 +51,62 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani return; } - let alignments = state.ecs().read_storage::(); - let agents = state.ecs().read_storage::(); - let mut already_has_invite = false; - let mut add_to_group = false; - // If client comp - if let (Some(client), Some(inviter_uid)) = (clients.get_mut(invitee), uids.get(entity)) - { - if client.invited_to_group.is_some() { - already_has_invite = true; - } else { - client.notify(ServerMsg::GroupInvite(*inviter_uid)); - client.invited_to_group = Some(entity); + // Disallow inviting entity that is already in your group + let groups = state.ecs().read_storage::(); + let group_manager = state.ecs().read_resource::(); + if groups.get(entity).map_or(false, |group| { + group_manager + .group_info(*group) + .map_or(false, |g| g.leader == entity) + && groups.get(invitee) == Some(group) + }) { + // Inform of failure + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg( + "Invite failed, can't invite someone already in your group".to_owned(), + )); } - // Would be cool to do this in agent system (e.g. add an invited - // component to replace the field on Client) - // TODO: move invites to component and make them time out - } else if matches!( - (alignments.get(invitee), agents.get(invitee)), - (Some(comp::Alignment::Npc), Some(_)) - ) { - add_to_group = true; - // Wipe agent state - drop(agents); - let _ = state - .ecs() - .write_storage() - .insert(invitee, comp::Agent::default()); - } else if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg("Invite rejected.".to_owned())); + return; } - if already_has_invite { + let mut pending_invites = state.ecs().write_storage::(); + + // Check if group max size is already reached + // Adding the current number of pending invites + if state + .ecs() + .read_storage() + .get(entity) + .copied() + .and_then(|group| { + // If entity is currently the leader of a full group then they can't invite + // anyone else + group_manager + .group_info(group) + .filter(|i| i.leader == entity) + .map(|i| i.num_members) + }) + .unwrap_or(1) as usize + + pending_invites.get(entity).map_or(0, |p| p.0.len()) + >= max_group_size as usize + { + // Inform inviter that they have reached the group size limit + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg( + "Invite failed, pending invites plus current group size have reached \ + the group size limit" + .to_owned(), + ), + ); + } + return; + } + + let agents = state.ecs().read_storage::(); + let mut invites = state.ecs().write_storage::(); + + if invites.contains(invitee) { // Inform inviter that there is already an invite if let Some(client) = clients.get_mut(entity) { client.notify( @@ -83,37 +114,94 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani .server_msg("This player already has a pending invite.".to_owned()), ); } + return; } - if add_to_group { - let mut group_manager = state.ecs().write_resource::(); - group_manager.add_group_member( - entity, - invitee, - &state.ecs().entities(), - &mut state.ecs().write_storage(), - &alignments, - &uids, - |entity, group_change| { - clients - .get_mut(entity) - .and_then(|c| { - group_change - .try_map(|e| uids.get(e).copied()) - .map(|g| (g, c)) - }) - .map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g))); + let mut invite_sent = false; + // Returns true if insertion was succesful + let mut send_invite = || { + match invites.insert(invitee, group::Invite(entity)) { + Err(err) => { + error!("Failed to insert Invite component: {:?}", err); + false }, - ); + Ok(_) => { + match pending_invites.entry(entity) { + Ok(entry) => { + entry + .or_insert_with(|| PendingInvites(Vec::new())) + .0 + .push((invitee, Instant::now() + INVITE_TIMEOUT_DUR)); + invite_sent = true; + true + }, + Err(err) => { + error!( + "Failed to get entry for pending invites component: {:?}", + err + ); + // Cleanup + invites.remove(invitee); + false + }, + } + }, + } + }; + + // If client comp + if let (Some(client), Some(inviter)) = + (clients.get_mut(invitee), uids.get(entity).copied()) + { + if send_invite() { + client.notify(ServerMsg::GroupInvite { + inviter, + timeout: PRESENTED_INVITE_TIMEOUT_DUR, + }); + } + } else if agents.contains(invitee) { + send_invite(); + } else { + if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()), + ); + } + } + + // Notify inviter that the invite is pending + if invite_sent { + if let Some(client) = clients.get_mut(entity) { + client.notify(ServerMsg::InvitePending(uid)); + } } }, GroupManip::Accept => { let mut clients = state.ecs().write_storage::(); let uids = state.ecs().read_storage::(); - if let Some(inviter) = clients - .get_mut(entity) - .and_then(|c| c.invited_to_group.take()) - { + let mut invites = state.ecs().write_storage::(); + if let Some(inviter) = invites.remove(entity).and_then(|invite| { + let inviter = invite.0; + let mut pending_invites = state.ecs().write_storage::(); + let pending = &mut pending_invites.get_mut(inviter)?.0; + // Check that inviter has a pending invite and remove it from the list + let invite_index = pending.iter().position(|p| p.0 == entity)?; + pending.swap_remove(invite_index); + // If no pending invites remain remove the component + if pending.is_empty() { + pending_invites.remove(inviter); + } + + Some(inviter) + }) { + if let (Some(client), Some(target)) = + (clients.get_mut(inviter), uids.get(entity).copied()) + { + client.notify(ServerMsg::InviteComplete { + target, + answer: InviteAnswer::Accepted, + }) + } let mut group_manager = state.ecs().write_resource::(); group_manager.add_group_member( inviter, @@ -137,14 +225,30 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani }, GroupManip::Decline => { let mut clients = state.ecs().write_storage::(); - if let Some(inviter) = clients - .get_mut(entity) - .and_then(|c| c.invited_to_group.take()) - { + let uids = state.ecs().read_storage::(); + let mut invites = state.ecs().write_storage::(); + if let Some(inviter) = invites.remove(entity).and_then(|invite| { + let inviter = invite.0; + let mut pending_invites = state.ecs().write_storage::(); + let pending = &mut pending_invites.get_mut(inviter)?.0; + // Check that inviter has a pending invite and remove it from the list + let invite_index = pending.iter().position(|p| p.0 == entity)?; + pending.swap_remove(invite_index); + // If no pending invites remain remove the component + if pending.is_empty() { + pending_invites.remove(inviter); + } + + Some(inviter) + }) { // Inform inviter of rejection - if let Some(client) = clients.get_mut(inviter) { - // TODO: say who declined the invite - client.notify(ChatType::Meta.server_msg("Invite declined.".to_owned())); + if let (Some(client), Some(target)) = + (clients.get_mut(inviter), uids.get(entity).copied()) + { + client.notify(ServerMsg::InviteComplete { + target, + answer: InviteAnswer::Declined, + }) } } }, diff --git a/server/src/events/player.rs b/server/src/events/player.rs index c81482c199..2c9a06e54a 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -59,7 +59,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { &state.ecs().entities(), &state.ecs().read_storage(), &state.ecs().read_storage(), - // Nothing actually changing + // Nothing actually changing since Uid is transferred |_, _| {}, ); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 9a43cb82db..c0b7c4cea6 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,6 +1,6 @@ #![deny(unsafe_code)] #![allow(clippy::option_map_unit_fn)] -#![feature(drain_filter, option_zip)] +#![feature(bool_to_option, drain_filter, option_zip)] pub mod alias_validator; pub mod chunk_generator; @@ -127,6 +127,7 @@ impl Server { state.ecs_mut().insert(sys::TerrainSyncTimer::default()); state.ecs_mut().insert(sys::TerrainTimer::default()); state.ecs_mut().insert(sys::WaypointTimer::default()); + state.ecs_mut().insert(sys::InviteTimeoutTimer::default()); state.ecs_mut().insert(sys::PersistenceTimer::default()); // System schedulers to control execution of systems @@ -508,12 +509,13 @@ impl Server { .nanos as i64; let terrain_nanos = self.state.ecs().read_resource::().nanos as i64; let waypoint_nanos = self.state.ecs().read_resource::().nanos as i64; + let invite_timeout_nanos = self.state.ecs().read_resource::().nanos as i64; let stats_persistence_nanos = self .state .ecs() .read_resource::() .nanos as i64; - let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos; + let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos + invite_timeout_nanos; // Report timing info self.tick_metrics @@ -575,6 +577,10 @@ impl Server { .tick_time .with_label_values(&["waypoint"]) .set(waypoint_nanos); + self.tick_metrics + .tick_time + .with_label_values(&["invite timeout"]) + .set(invite_timeout_nanos); self.tick_metrics .tick_time .with_label_values(&["persistence:stats"]) @@ -656,7 +662,6 @@ impl Server { network_error: std::sync::atomic::AtomicBool::new(false), last_ping: self.state.get_time(), login_msg_sent: false, - invited_to_group: None, }; if self.settings().max_players @@ -685,6 +690,7 @@ impl Server { .create_entity_package(entity, None, None, None), server_info: self.get_server_info(), time_of_day: *self.state.ecs().read_resource(), + max_group_size: self.settings().max_player_group_size, world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()), recipe_book: (&*default_recipe_book()).clone(), }); diff --git a/server/src/settings.rs b/server/src/settings.rs index 8bc0c6f3a5..06dec6e3a9 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -26,6 +26,7 @@ pub struct ServerSettings { pub persistence_db_dir: String, pub max_view_distance: Option, pub banned_words_files: Vec, + pub max_player_group_size: u32, } impl Default for ServerSettings { @@ -65,6 +66,7 @@ impl Default for ServerSettings { persistence_db_dir: "saves".to_owned(), max_view_distance: Some(30), banned_words_files: Vec::new(), + max_player_group_size: 6, } } } diff --git a/server/src/sys/invite_timeout.rs b/server/src/sys/invite_timeout.rs new file mode 100644 index 0000000000..7a701b03a4 --- /dev/null +++ b/server/src/sys/invite_timeout.rs @@ -0,0 +1,71 @@ +use super::SysTimer; +use crate::client::Client; +use common::{ + comp::group::{Invite, PendingInvites}, + msg::{InviteAnswer, ServerMsg}, + sync::Uid, +}; +use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage}; + +/// This system removes timed out group invites +pub struct Sys; +impl<'a> System<'a> for Sys { + #[allow(clippy::type_complexity)] // TODO: Pending review in #587 + type SystemData = ( + Entities<'a>, + WriteStorage<'a, Invite>, + WriteStorage<'a, PendingInvites>, + WriteStorage<'a, Client>, + ReadStorage<'a, Uid>, + Write<'a, SysTimer>, + ); + + fn run( + &mut self, + (entities, mut invites, mut pending_invites, mut clients, uids, mut timer): Self::SystemData, + ) { + timer.start(); + + let now = std::time::Instant::now(); + + let timed_out_invites = (&entities, &invites) + .join() + .filter_map(|(invitee, Invite(inviter))| { + // Retrieve timeout invite from pending invites + let pending = &mut pending_invites.get_mut(*inviter)?.0; + let index = pending.iter().position(|p| p.0 == invitee)?; + + // Stop if not timed out + if pending[index].1 > now { + return None; + } + + // Remove pending entry + pending.swap_remove(index); + + // If no pending invites remain remove the component + if pending.is_empty() { + pending_invites.remove(*inviter); + } + + // Inform inviter of timeout + if let (Some(client), Some(target)) = + (clients.get_mut(*inviter), uids.get(invitee).copied()) + { + client.notify(ServerMsg::InviteComplete { + target, + answer: InviteAnswer::TimedOut, + }) + } + + Some(invitee) + }) + .collect::>(); + + for entity in timed_out_invites { + invites.remove(entity); + } + + timer.end(); + } +} diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 5afef7795c..6b98fc9edb 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -1,4 +1,5 @@ pub mod entity_sync; +pub mod invite_timeout; pub mod message; pub mod object; pub mod persistence; @@ -21,6 +22,7 @@ pub type SubscriptionTimer = SysTimer; pub type TerrainTimer = SysTimer; pub type TerrainSyncTimer = SysTimer; pub type WaypointTimer = SysTimer; +pub type InviteTimeoutTimer = SysTimer; pub type PersistenceTimer = SysTimer; pub type PersistenceScheduler = SysScheduler; @@ -32,12 +34,14 @@ pub type PersistenceScheduler = SysScheduler; //const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys"; const TERRAIN_SYS: &str = "server_terrain_sys"; const WAYPOINT_SYS: &str = "server_waypoint_sys"; +const INVITE_TIMEOUT_SYS: &str = "server_invite_timeout_sys"; const PERSISTENCE_SYS: &str = "server_persistence_sys"; const OBJECT_SYS: &str = "server_object_sys"; pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]); dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]); + dispatch_builder.add(invite_timeout::Sys, INVITE_TIMEOUT_SYS, &[]); dispatch_builder.add(persistence::Sys, PERSISTENCE_SYS, &[]); dispatch_builder.add(object::Sys, OBJECT_SYS, &[]); } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index a35a37db83..fd47ac3d6e 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -182,23 +182,22 @@ impl<'a> Widget for Group<'a> { .crop_kids() .set(state.ids.bg, ui); } - if open_invite.is_some() { + if let Some((_, timeout_start, timeout_dur)) = open_invite { // Group Menu button Button::image(self.imgs.group_icon) .w_h(49.0, 26.0) .bottom_left_with_margins_on(ui.window, 10.0, 490.0) .set(state.ids.group_button, ui); // Show timeout bar - let max_time = 90.0; - let time = 50.0; - let progress_perc = time / max_time; + let timeout_progress = + 1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32(); Image::new(self.imgs.progress_frame) .w_h(100.0, 10.0) .middle_of(state.ids.bg) .color(Some(UI_MAIN)) .set(state.ids.timeout_bg, ui); Image::new(self.imgs.progress) - .w_h(98.0 * progress_perc, 8.0) + .w_h(98.0 * timeout_progress as f64, 8.0) .top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0) .color(Some(UI_HIGHLIGHT_0)) .set(state.ids.timeout, ui); @@ -613,7 +612,7 @@ impl<'a> Widget for Group<'a> { // into the maximum group size. } } - if let Some(invite_uid) = open_invite { + if let Some((invite_uid, _, _)) = open_invite { self.show.group = true; // Auto open group menu // TODO: add group name here too // Invite text diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index e1158019a4..c1dc3a7eb3 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -8,7 +8,7 @@ use crate::{ ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, }; use client::{self, Client}; -use common::sync::Uid; +use common::{comp::group, sync::Uid}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, @@ -467,68 +467,90 @@ impl<'a> Widget for Social<'a> { } // Invite Button - let selected_ingame = state - .selected_uid - .as_ref() - .map(|(s, _)| *s) - .filter(|selected| { - self.client - .player_list - .get(selected) - .map_or(false, |selected_player| { - selected_player.is_online && selected_player.character.is_some() + let is_leader_or_not_in_group = self + .client + .group_info() + .map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid)); + + let current_members = self + .client + .group_members() + .iter() + .filter(|(_, role)| matches!(role, group::Role::Member)) + .count() + + 1; + let current_invites = self.client.pending_invites().len(); + let max_members = self.client.max_group_size() as usize; + let group_not_full = current_members + current_invites < max_members; + let selected_to_invite = (is_leader_or_not_in_group && group_not_full) + .then(|| { + state + .selected_uid + .as_ref() + .map(|(s, _)| *s) + .filter(|selected| { + self.client + .player_list + .get(selected) + .map_or(false, |selected_player| { + selected_player.is_online && selected_player.character.is_some() + }) + }) + .or_else(|| { + self.selected_entity + .and_then(|s| self.client.state().read_component_copied(s.0)) + }) + .filter(|selected| { + // Prevent inviting entities already in the same group + !self.client.group_members().contains_key(selected) }) }) - .or_else(|| { - self.selected_entity - .and_then(|s| self.client.state().read_component_copied(s.0)) - }); - // TODO: Prevent inviting players with the same group uid - // TODO: Show current amount of group members as a tooltip for the invite button - // if the player is the group leader TODO: Grey out the invite - // button if the group has 6/6 members - let current_members = 4; - let tooltip_txt = if selected_ingame.is_some() { - format!( - "{}/6 {}", - ¤t_members, - &self.localized_strings.get("hud.group.members") - ) - } else { - (&self.localized_strings.get("hud.group.members")).to_string() - }; - if Button::image(self.imgs.button) + .flatten(); + + let invite_button = Button::image(self.imgs.button) .w_h(106.0, 26.0) .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0) - .hover_image(if selected_ingame.is_some() { + .hover_image(if selected_to_invite.is_some() { self.imgs.button_hover } else { self.imgs.button }) - .press_image(if selected_ingame.is_some() { + .press_image(if selected_to_invite.is_some() { self.imgs.button_press } else { self.imgs.button }) - .label(&self.localized_strings.get("hud.group.invite")) + .label(self.localized_strings.get("hud.group.invite")) .label_y(conrod_core::position::Relative::Scalar(3.0)) - .label_color(if selected_ingame.is_some() { + .label_color(if selected_to_invite.is_some() { TEXT_COLOR } else { TEXT_COLOR_3 }) - .image_color(if selected_ingame.is_some() { + .image_color(if selected_to_invite.is_some() { TEXT_COLOR } else { TEXT_COLOR_3 }) .label_font_size(self.fonts.cyri.scale(15)) - .label_font_id(self.fonts.cyri.conrod_id) - .with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip) - .set(state.ids.invite_button, ui) - .was_clicked() + .label_font_id(self.fonts.cyri.conrod_id); + + if if self.client.group_info().is_some() { + let tooltip_txt = format!( + "{}/{} {}", + current_members + current_invites, + max_members, + &self.localized_strings.get("hud.group.members") + ); + invite_button + .with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip) + .set(state.ids.invite_button, ui) + } else { + invite_button.set(state.ids.invite_button, ui) + } + .was_clicked() { - if let Some(uid) = selected_ingame { + if let Some(uid) = selected_to_invite { events.push(Event::Invite(uid)); state.update(|s| { s.selected_uid = None; From af8560ea3670aa003973e47f0ea1a7adf36c6401 Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 6 Aug 2020 23:53:19 -0400 Subject: [PATCH 25/71] Remove pets from the group exp division calculation but still give them exp, fix bug with group code when pets are deleted --- common/src/comp/group.rs | 25 +++++++++++++++------ server/src/events/entity_manipulation.rs | 28 +++++++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 18de30ed8b..bf11ad7f58 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -462,10 +462,21 @@ impl GroupManager { } if let Some(info) = self.group_info_mut(group) { - info.num_members -= 1; + // If not pet, decrement number of members + if !matches!(alignments.get(member), Some(Alignment::Owned(owner)) if uids.get(member).map_or(true, |uid| uid != owner)) + { + if info.num_members > 0 { + info.num_members -= 1; + } else { + error!("Group with invalid number of members") + } + } + + let mut remaining_count = 0; // includes pets // Inform remaining members - members(group, &*groups, entities, alignments, uids).for_each( - |(e, role)| match role { + members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| { + remaining_count += 1; + match role { Role::Member => { notifier(e, ChangeNotification::Removed(member)); leaving_pets.iter().for_each(|p| { @@ -473,16 +484,16 @@ impl GroupManager { }) }, Role::Pet => {}, - }, - ); + } + }); // If leader is the last one left then disband the group // Assumes last member is the leader - if info.num_members == 1 { + if remaining_count == 1 { let leader = info.leader; self.remove_group(group); groups.remove(leader); notifier(leader, ChangeNotification::NoGroup); - } else if info.num_members == 0 { + } else if remaining_count == 0 { error!("Somehow group has no members") } } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index b6900b70fd..00b1810ad2 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2,8 +2,8 @@ use crate::{client::Client, Server, SpawnPoint, StateExt}; use common::{ assets, comp::{ - self, item::lottery::Lottery, object, Body, Damage, DamageSource, Group, HealthChange, - HealthSource, Player, Pos, Stats, + self, item::lottery::Lottery, object, Alignment, Body, Damage, DamageSource, Group, + HealthChange, HealthSource, Player, Pos, Stats, }, msg::{PlayerListUpdate, ServerMsg}, state::BlockChange, @@ -94,19 +94,35 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc // Distribute EXP to group let positions = state.ecs().read_storage::(); + let alignments = state.ecs().read_storage::(); + let uids = state.ecs().read_storage::(); if let (Some(attacker_group), Some(pos)) = (attacker_group, positions.get(entity)) { // TODO: rework if change to groups makes it easier to iterate entities in a // group - let members_in_range = (&state.ecs().entities(), &groups, &positions) + let mut num_not_pets_in_range = 0; + let members_in_range = ( + &state.ecs().entities(), + &groups, + &positions, + alignments.maybe(), + &uids, + ) .join() - .filter(|(entity, group, member_pos)| { + .filter(|(entity, group, member_pos, _, _)| { + // Check if: in group, not main attacker, and in range *group == attacker_group && *entity != attacker && pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) }) - .map(|(entity, _, _)| entity) + .map(|(entity, _, _, alignment, uid)| { + if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) { + num_not_pets_in_range += 1; + } + + entity + }) .collect::>(); - let exp = exp_reward / (members_in_range.len() as f32 + ATTACKER_EXP_WEIGHT); + let exp = exp_reward / (num_not_pets_in_range as f32 + ATTACKER_EXP_WEIGHT); exp_reward = exp * ATTACKER_EXP_WEIGHT; members_in_range.into_iter().for_each(|e| { if let Some(stats) = stats.get_mut(e) { From a7df000a6f6797e92f7810c15ea38fdc893401c4 Mon Sep 17 00:00:00 2001 From: Imbris Date: Fri, 7 Aug 2020 00:33:24 -0400 Subject: [PATCH 26/71] Fireballs no longer damage group members --- client/src/lib.rs | 2 +- common/src/event.rs | 1 + common/src/sys/projectile.rs | 2 ++ server/src/cmd.rs | 1 + server/src/events/entity_manipulation.rs | 28 +++++++++++++++++++----- server/src/events/mod.rs | 9 +++++--- server/src/sys/object.rs | 1 + 7 files changed, 35 insertions(+), 9 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index aec0cb919c..f3e7a0a33f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1050,7 +1050,7 @@ impl Client { self.group_leader = Some(leader); self.group_members = members.into_iter().collect(); // Currently add/remove messages treat client as an implicit member - // of the group whereas this message explicitly included them so to + // of the group whereas this message explicitly includes them so to // be consistent for now we will remove the client from the // received hashset if let Some(uid) = self.uid() { diff --git a/common/src/event.rs b/common/src/event.rs index 9a07615c68..943757a2a7 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -25,6 +25,7 @@ pub enum ServerEvent { pos: Vec3, power: f32, owner: Option, + friendly_damage: bool, }, Damage { uid: Uid, diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs index ae6817cf35..20f96fc311 100644 --- a/common/src/sys/projectile.rs +++ b/common/src/sys/projectile.rs @@ -70,6 +70,7 @@ impl<'a> System<'a> for Sys { pos: pos.0, power, owner: projectile.owner, + friendly_damage: false, }) }, projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy { @@ -131,6 +132,7 @@ impl<'a> System<'a> for Sys { pos: pos.0, power, owner: projectile.owner, + friendly_damage: false, }) }, projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3c5601f4e0..81a19a2a49 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1006,6 +1006,7 @@ fn handle_explosion( pos: pos.0, power, owner: ecs.read_storage::().get(target).copied(), + friendly_damage: true, }) }, None => server.notify_client( diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 00b1810ad2..0a73123a28 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -7,12 +7,12 @@ use common::{ }, msg::{PlayerListUpdate, ServerMsg}, state::BlockChange, - sync::{Uid, WorldSyncExt}, + sync::{Uid, UidAllocator, WorldSyncExt}, sys::combat::BLOCK_ANGLE, terrain::{Block, TerrainGrid}, vol::{ReadVol, Vox}, }; -use specs::{join::Join, Entity as EcsEntity, WorldExt}; +use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt}; use tracing::error; use vek::Vec3; @@ -277,11 +277,25 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { } } -pub fn handle_explosion(server: &Server, pos: Vec3, power: f32, owner: Option) { +pub fn handle_explosion( + server: &Server, + pos: Vec3, + power: f32, + owner: Option, + friendly_damage: bool, +) { // Go through all other entities let hit_range = 3.0 * power; let ecs = &server.state.ecs(); - for (pos_b, ori_b, character_b, stats_b, loadout_b) in ( + + let owner_entity = owner.and_then(|uid| { + ecs.read_resource::() + .retrieve_entity_internal(uid.into()) + }); + let groups = ecs.read_storage::(); + + for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in ( + &ecs.entities(), &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), @@ -293,9 +307,13 @@ pub fn handle_explosion(server: &Server, pos: Vec3, power: f32, owner: Opti let distance_squared = pos.distance_squared(pos_b.0); // Check if it is a hit if !stats_b.is_dead - // Spherical wedge shaped attack field // RADIUS && distance_squared < hit_range.powi(2) + // Skip if they are in the same group and friendly_damage is turned off for the + // explosion + && (friendly_damage || !owner_entity + .and_then(|e| groups.get(e)) + .map_or(false, |group_a| Some(group_a) == groups.get(entity_b))) { // Weapon gives base damage let dmg = (1.0 - distance_squared / hit_range.powi(2)) * power * 130.0; diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 55783d5196..412d9a536a 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -50,9 +50,12 @@ impl Server { for event in events { match event { - ServerEvent::Explosion { pos, power, owner } => { - handle_explosion(&self, pos, power, owner) - }, + ServerEvent::Explosion { + pos, + power, + owner, + friendly_damage, + } => handle_explosion(&self, pos, power, owner, friendly_damage), ServerEvent::Shoot { entity, dir, diff --git a/server/src/sys/object.rs b/server/src/sys/object.rs index be986f8e6e..ea3c2d3ad5 100644 --- a/server/src/sys/object.rs +++ b/server/src/sys/object.rs @@ -39,6 +39,7 @@ impl<'a> System<'a> for Sys { pos: pos.0, power: 4.0, owner: *owner, + friendly_damage: true, }); } }, From 2faa1cd6c1882e251b1ad79f8e250b6ad23164de Mon Sep 17 00:00:00 2001 From: Imbris Date: Fri, 7 Aug 2020 00:48:35 -0400 Subject: [PATCH 27/71] Add info message in chat about group chat when joining a group --- client/src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/lib.rs b/client/src/lib.rs index f3e7a0a33f..0fff1c773a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1026,6 +1026,15 @@ impl Client { // the view distance match change_notification { Added(uid, role) => { + // Check if this is a newly formed group by looking for absence of + // other non pet group members + if !matches!(role, group::Role::Pet) + && !self.group_members.values().any(|r| !matches!(r, group::Role::Pet)) + { + frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg( + "Type /g or /group to chat with your group members" + ))); + } if self.group_members.insert(uid, role) == Some(role) { warn!( "Received msg to add uid {} to the group members but they \ From 2608217e837e1f3e32e134e8a8dc10dd153b29f9 Mon Sep 17 00:00:00 2001 From: Imbris Date: Fri, 7 Aug 2020 01:00:09 -0400 Subject: [PATCH 28/71] Make clippy happy, fmt even though it is set to fmt on save in my editor...." --- CHANGELOG.md | 1 - client/src/lib.rs | 149 +++++++++++++++++-------------- server/src/events/group_manip.rs | 21 +++-- server/src/lib.rs | 11 ++- voxygen/src/hud/group.rs | 10 +-- voxygen/src/hud/skillbar.rs | 11 +-- 6 files changed, 110 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc83751c0..7c03261b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add detection of entities under the cursor - Functional group-system with exp-sharing and disabled damage to group members - ### Changed - Improved camera aiming diff --git a/client/src/lib.rs b/client/src/lib.rs index 0fff1c773a..98be27bd60 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -21,9 +21,9 @@ use common::{ InventoryManip, InventoryUpdateEvent, }, msg::{ - validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, - PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, - MAX_BYTES_CHAT_MSG, InviteAnswer, + validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, InviteAnswer, + Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, + ServerMsg, MAX_BYTES_CHAT_MSG, }, recipe::RecipeBook, state::State, @@ -135,48 +135,49 @@ impl Client { let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?; // Wait for initial sync - let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(async { - loop { - match stream.recv().await? { - ServerMsg::InitialSync { - entity_package, - server_info, - time_of_day, - max_group_size, - world_map: (map_size, world_map), - recipe_book, - } => { - // TODO: Display that versions don't match in Voxygen - if &server_info.git_hash != *common::util::GIT_HASH { - warn!( - "Server is running {}[{}], you are running {}[{}], versions might \ - be incompatible!", - server_info.git_hash, - server_info.git_date, - common::util::GIT_HASH.to_string(), - common::util::GIT_DATE.to_string(), - ); - } + let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on( + async { + loop { + match stream.recv().await? { + ServerMsg::InitialSync { + entity_package, + server_info, + time_of_day, + max_group_size, + world_map: (map_size, world_map), + recipe_book, + } => { + // TODO: Display that versions don't match in Voxygen + if &server_info.git_hash != *common::util::GIT_HASH { + warn!( + "Server is running {}[{}], you are running {}[{}], versions \ + might be incompatible!", + server_info.git_hash, + server_info.git_date, + common::util::GIT_HASH.to_string(), + common::util::GIT_DATE.to_string(), + ); + } - debug!("Auth Server: {:?}", server_info.auth_provider); + debug!("Auth Server: {:?}", server_info.auth_provider); - // Initialize `State` - let mut state = State::default(); - // Client-only components - state - .ecs_mut() - .register::>(); + // Initialize `State` + let mut state = State::default(); + // Client-only components + state + .ecs_mut() + .register::>(); - let entity = state.ecs_mut().apply_entity_package(entity_package); - *state.ecs_mut().write_resource() = time_of_day; + let entity = state.ecs_mut().apply_entity_package(entity_package); + *state.ecs_mut().write_resource() = time_of_day; - assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize); - let mut world_map_raw = - vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/]; - LittleEndian::write_u32_into(&world_map, &mut world_map_raw); - debug!("Preparing image..."); - let world_map = Arc::new( - image::DynamicImage::ImageRgba8({ + assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize); + let mut world_map_raw = + vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/]; + LittleEndian::write_u32_into(&world_map, &mut world_map_raw); + debug!("Preparing image..."); + let world_map = Arc::new( + image::DynamicImage::ImageRgba8({ // Should not fail if the dimensions are correct. let world_map = image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw); @@ -185,25 +186,26 @@ impl Client { // Flip the image, since Voxygen uses an orientation where rotation from // positive x axis to positive y axis is counterclockwise around the z axis. .flipv(), - ); - debug!("Done preparing image..."); + ); + debug!("Done preparing image..."); - break Ok(( - state, - entity, - server_info, - (world_map, map_size), - recipe_book, - max_group_size, - )); - }, - ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), - err => { - warn!("whoops, server mad {:?}, ignoring", err); - }, + break Ok(( + state, + entity, + server_info, + (world_map, map_size), + recipe_book, + max_group_size, + )); + }, + ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), + err => { + warn!("whoops, server mad {:?}, ignoring", err); + }, + } } - } - })?; + }, + )?; stream.send(ClientMsg::Ping)?; @@ -443,7 +445,9 @@ impl Client { pub fn max_group_size(&self) -> u32 { self.max_group_size } - pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { self.group_invite } + pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> { + self.group_invite + } pub fn group_info(&self) -> Option<(String, Uid)> { self.group_leader.map(|l| ("Group".into(), l)) // TODO @@ -772,7 +776,10 @@ impl Client { // 3) Update client local data // Check if the group invite has timed out and remove if so - if self.group_invite.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) { + if self + .group_invite + .map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur) + { self.group_invite = None; } @@ -1028,11 +1035,14 @@ impl Client { Added(uid, role) => { // Check if this is a newly formed group by looking for absence of // other non pet group members - if !matches!(role, group::Role::Pet) - && !self.group_members.values().any(|r| !matches!(r, group::Role::Pet)) + if !matches!(role, group::Role::Pet) + && !self + .group_members + .values() + .any(|r| !matches!(r, group::Role::Pet)) { frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg( - "Type /g or /group to chat with your group members" + "Type /g or /group to chat with your group members", ))); } if self.group_members.insert(uid, role) == Some(role) { @@ -1079,21 +1089,24 @@ impl Client { if !self.pending_invites.insert(uid) { warn!("Received message about pending invite that was already pending"); } - } + }, ServerMsg::InviteComplete { target, answer } => { if !self.pending_invites.remove(&target) { - warn!("Received completed invite message for invite that was not in the list of pending invites") + warn!( + "Received completed invite message for invite that was not in the \ + list of pending invites" + ) } // TODO: expose this as a new event variant instead of going // through the chat let msg = match answer { // TODO: say who accepted/declined/timed out the invite InviteAnswer::Accepted => "Invite accepted", - InviteAnswer::Declined => "Invite declined", - InviteAnswer::TimedOut => "Invite timed out", + InviteAnswer::Declined => "Invite declined", + InviteAnswer::TimedOut => "Invite timed out", }; frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg))); - } + }, ServerMsg::Ping => { self.singleton_stream.send(ClientMsg::Pong)?; }, diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index d1a030ce1c..fb028729de 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -54,12 +54,13 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani // Disallow inviting entity that is already in your group let groups = state.ecs().read_storage::(); let group_manager = state.ecs().read_resource::(); - if groups.get(entity).map_or(false, |group| { + let already_in_same_group = groups.get(entity).map_or(false, |group| { group_manager .group_info(*group) .map_or(false, |g| g.leader == entity) && groups.get(invitee) == Some(group) - }) { + }); + if already_in_same_group { // Inform of failure if let Some(client) = clients.get_mut(entity) { client.notify(ChatType::Meta.server_msg( @@ -73,7 +74,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani // Check if group max size is already reached // Adding the current number of pending invites - if state + let group_size_limit_reached = state .ecs() .read_storage() .get(entity) @@ -88,8 +89,8 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani }) .unwrap_or(1) as usize + pending_invites.get(entity).map_or(0, |p| p.0.len()) - >= max_group_size as usize - { + >= max_group_size as usize; + if group_size_limit_reached { // Inform inviter that they have reached the group size limit if let Some(client) = clients.get_mut(entity) { client.notify( @@ -161,12 +162,10 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani } } else if agents.contains(invitee) { send_invite(); - } else { - if let Some(client) = clients.get_mut(entity) { - client.notify( - ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()), - ); - } + } else if let Some(client) = clients.get_mut(entity) { + client.notify( + ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()), + ); } // Notify inviter that the invite is pending diff --git a/server/src/lib.rs b/server/src/lib.rs index c0b7c4cea6..7d27b933ab 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -509,13 +509,18 @@ impl Server { .nanos as i64; let terrain_nanos = self.state.ecs().read_resource::().nanos as i64; let waypoint_nanos = self.state.ecs().read_resource::().nanos as i64; - let invite_timeout_nanos = self.state.ecs().read_resource::().nanos as i64; + let invite_timeout_nanos = self + .state + .ecs() + .read_resource::() + .nanos as i64; let stats_persistence_nanos = self .state .ecs() .read_resource::() .nanos as i64; - let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos + invite_timeout_nanos; + let total_sys_ran_in_dispatcher_nanos = + terrain_nanos + waypoint_nanos + invite_timeout_nanos; // Report timing info self.tick_metrics @@ -690,7 +695,7 @@ impl Server { .create_entity_package(entity, None, None, None), server_info: self.get_server_info(), time_of_day: *self.state.ecs().read_resource(), - max_group_size: self.settings().max_player_group_size, + max_group_size: self.settings().max_player_group_size, world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()), recipe_book: (&*default_recipe_book()).clone(), }); diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index fd47ac3d6e..26060b3084 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -118,13 +118,13 @@ impl<'a> Widget for Group<'a> { } } - #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 - fn style(&self) -> Self::Style { () } + fn style(&self) -> Self::Style {} //TODO: Disband groups when there's only one member in them //TODO: Always send health, energy, level and position of group members to the // client #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + #[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; @@ -326,11 +326,11 @@ impl<'a> Widget for Group<'a> { 21..=40 => LOW_HP_COLOR, _ => HP_COLOR, }; - let lead = if uid == leader { true } else { false }; + let is_leader = uid == leader; // Don't show panel for the player! // Panel BG back.w_h(152.0, 36.0) - .color(if lead { + .color(if is_leader { Some(ERROR_COLOR) } else { Some(TEXT_COLOR) @@ -396,7 +396,7 @@ impl<'a> Widget for Group<'a> { .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 lead { ERROR_COLOR } else { GROUP_COLOR }) + .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 { diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 9b0654eb91..907ac4a5af 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -1164,14 +1164,14 @@ impl<'a> Widget for Skillbar<'a> { }; Image::new(self.imgs.bar_content) .w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale) - .color(Some(health_col)) + .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); + .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) @@ -1186,7 +1186,8 @@ impl<'a> Widget for Skillbar<'a> { 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.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!( @@ -1194,7 +1195,7 @@ impl<'a> Widget for Skillbar<'a> { 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(); @@ -1210,7 +1211,7 @@ impl<'a> Widget for Skillbar<'a> { .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(state.ids.health_text, ui); + .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)) From 74ace74d5a149be3c72ac2d910353540839d2885 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Fri, 7 Aug 2020 15:04:43 +0200 Subject: [PATCH 29/71] Change to social window selection visuals --- voxygen/src/hud/social.rs | 57 ++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index c1dc3a7eb3..78a9ef46c4 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -287,7 +287,7 @@ impl<'a> Widget for Social<'a> { .set(state.ids.zones_align, ui); Scrollbar::y_axis(state.ids.online_align) .thickness(4.0) - .color(UI_HIGHLIGHT_0) + .color(Color::Rgba(0.79, 1.09, 1.09, 0.0)) .set(state.ids.scrollbar, ui); // // Headlines @@ -413,7 +413,7 @@ impl<'a> Widget for Social<'a> { } else { button.down_from(state.ids.player_names[i - 1], 1.0) }; - if button + button .w_h(133.0, 20.0) .hover_image(if selected { self.imgs.selection @@ -427,33 +427,40 @@ impl<'a> Widget for Social<'a> { }) .label(&name_text) .label_font_size(self.fonts.cyri.scale(14)) - .label_y(conrod_core::position::Relative::Scalar(0.0)) + .label_y(conrod_core::position::Relative::Scalar(1.0)) .label_font_id(self.fonts.cyri.conrod_id) .label_color(TEXT_COLOR) - .set(state.ids.player_names[i], ui) - .was_clicked() - {}; - let level_txt = if i == 0 { - Text::new(&level).mid_top_with_margin_on(state.ids.levels_align, 4.0) + .set(state.ids.player_names[i], ui); + // Player Levels + Button::image(if !selected { + self.imgs.nothing } else { - Text::new(&level).down_from(state.ids.player_levels[i - 1], 4.0) - }; - level_txt - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.player_levels[i], ui); - let zone_txt = if i == 0 { - Text::new(&zone_name).mid_top_with_margin_on(state.ids.zones_align, 4.0) + self.imgs.selection + }) + .w_h(39.0, 20.0) + .right_from(state.ids.player_names[i], 2.0) + .label(&level) + .label_font_size(self.fonts.cyri.scale(14)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .label_y(conrod_core::position::Relative::Scalar(1.0)) + .parent(state.ids.levels_align) + .set(state.ids.player_levels[i], ui); + // Player Zones + Button::image(if !selected { + self.imgs.nothing } else { - Text::new(&zone_name).down_from(state.ids.player_zones[i - 1], 4.0) - }; - zone_txt - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.player_zones[i], ui); - + self.imgs.selection + }) + .w_h(94.0, 20.0) + .right_from(state.ids.player_levels[i], 2.0) + .label(&zone_name) + .label_font_size(self.fonts.cyri.scale(14)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .label_y(conrod_core::position::Relative::Scalar(1.0)) + .parent(state.ids.zones_align) + .set(state.ids.player_zones[i], ui); // Check for click if ui .widget_input(state.ids.player_names[i]) From 142b386628bf095f24e9374bba8cf88a3595b646 Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Fri, 7 Aug 2020 21:27:15 +0200 Subject: [PATCH 30/71] address comments --- .cargo/config | 1 + assets/voxygen/i18n/de_DE.ron | 2 +- client/src/lib.rs | 16 ++++++++ common/src/cmd.rs | 8 ---- server/src/cmd.rs | 65 ------------------------------ voxygen/src/hud/settings_window.rs | 4 +- 6 files changed, 20 insertions(+), 76 deletions(-) diff --git a/.cargo/config b/.cargo/config index e4f3071f5c..51f84ab9e9 100644 --- a/.cargo/config +++ b/.cargo/config @@ -6,4 +6,5 @@ rustflags = [ [alias] generate = "run --package tools --" test-server = "run --bin veloren-server-cli --no-default-features" +server = "run --bin veloren-server-cli" diff --git a/assets/voxygen/i18n/de_DE.ron b/assets/voxygen/i18n/de_DE.ron index 11c6caabb0..f1f3f9da17 100644 --- a/assets/voxygen/i18n/de_DE.ron +++ b/assets/voxygen/i18n/de_DE.ron @@ -308,7 +308,7 @@ magischen Gegenstände ergattern?"#, "hud.settings.unbound": "-", "hud.settings.reset_keybinds": "Auf Standard zurücksetzen", - "hud.social": "Andere Spieler ", + "hud.social": "Andere Spieler", "hud.social.online": "Online", "hud.social.friends": "Freunde", "hud.social.not_yet_available": "Noch nicht verfügbar", diff --git a/client/src/lib.rs b/client/src/lib.rs index 98be27bd60..404c2a1686 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1045,6 +1045,14 @@ impl Client { "Type /g or /group to chat with your group members", ))); } + if let Some(player_info) = self.player_list.get(&uid) { + frontend_events.push(Event::Chat( + comp::ChatType::GroupMeta("Group".into()).chat_msg(format!( + "[{}] joined group", + player_info.player_alias + )), + )); + } if self.group_members.insert(uid, role) == Some(role) { warn!( "Received msg to add uid {} to the group members but they \ @@ -1054,6 +1062,14 @@ impl Client { } }, Removed(uid) => { + if let Some(player_info) = self.player_list.get(&uid) { + frontend_events.push(Event::Chat( + comp::ChatType::GroupMeta("Group".into()).chat_msg(format!( + "[{}] left group", + player_info.player_alias + )), + )); + } if self.group_members.remove(&uid).is_none() { warn!( "Received msg to remove uid {} from group members but by they \ diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 04ead5547d..6d8ed3af46 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -50,7 +50,6 @@ pub enum ChatCommand { Health, Help, JoinFaction, - //JoinGroup, Jump, Kill, KillNpcs, @@ -92,7 +91,6 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Health, ChatCommand::Help, ChatCommand::JoinFaction, - //ChatCommand::JoinGroup, ChatCommand::Jump, ChatCommand::Kill, ChatCommand::KillNpcs, @@ -246,11 +244,6 @@ impl ChatCommand { "Join/leave the specified faction", NoAdmin, ), - //ChatCommand::JoinGroup => ChatCommandData::new( - // vec![Any("group", Optional)], - // "Join/leave the specified group", - // NoAdmin, - //), ChatCommand::Jump => cmd( vec![ Float("x", 0.0, Required), @@ -383,7 +376,6 @@ impl ChatCommand { ChatCommand::Group => "group", ChatCommand::Health => "health", ChatCommand::JoinFaction => "join_faction", - //ChatCommand::JoinGroup => "join_group", ChatCommand::Help => "help", ChatCommand::Jump => "jump", ChatCommand::Kill => "kill", diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 81a19a2a49..b580cfbd98 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -77,7 +77,6 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Health => handle_health, ChatCommand::Help => handle_help, ChatCommand::JoinFaction => handle_join_faction, - //ChatCommand::JoinGroup => handle_join_group, ChatCommand::Jump => handle_jump, ChatCommand::Kill => handle_kill, ChatCommand::KillNpcs => handle_kill_npcs, @@ -588,7 +587,6 @@ fn handle_spawn( comp::Alignment::Npc | comp::Alignment::Tame => { Some(comp::group::NPC) }, - // TODO: handle comp::Alignment::Owned(_) => unreachable!(), } { let _ = @@ -1362,69 +1360,6 @@ fn handle_join_faction( } } -// TODO: it might be useful to copy the GroupMeta messages elsewhere -/*fn handle_join_group( - server: &mut Server, - client: EcsEntity, - target: EcsEntity, - args: String, - action: &ChatCommand, -) { - if client != target { - // This happens when [ab]using /sudo - server.notify_client( - client, - ChatType::CommandError.server_msg("It's rude to impersonate people"), - ); - return; - } - if let Some(alias) = server - .state - .ecs() - .read_storage::() - .get(target) - .map(|player| player.alias.clone()) - { - let group_leave = if let Ok(group) = scan_fmt!(&args, &action.arg_fmt(), String) { - let mode = comp::ChatMode::Group(group.clone()); - let _ = server.state.ecs().write_storage().insert(client, mode); - let group_leave = server - .state - .ecs() - .write_storage() - .insert(client, comp::ChatGroup(group.clone())) - .ok() - .flatten() - .map(|f| f.0); - server.state.send_chat( - ChatType::GroupMeta(group.clone()) - .chat_msg(format!("[{}] joined group ({})", alias, group)), - ); - group_leave - } else { - let mode = comp::ChatMode::default(); - let _ = server.state.ecs().write_storage().insert(client, mode); - server - .state - .ecs() - .write_storage() - .remove(client) - .map(|comp::ChatGroup(f)| f) - }; - if let Some(group) = group_leave { - server.state.send_chat( - ChatType::GroupMeta(group.clone()) - .chat_msg(format!("[{}] left group ({})", alias, group)), - ); - } - } else { - server.notify_client( - client, - ChatType::CommandError.server_msg("Could not find your player alias"), - ); - } -}*/ - #[cfg(not(feature = "worldgen"))] fn handle_debug_column( server: &mut Server, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 06c1b24e94..142b4d0984 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -1190,9 +1190,9 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.chat_char_name_text, ui); - // Show account name in chat + // TODO Show account name in chat - // Show account names in social window + // TODO Show account names in social window // Language select drop down Text::new(&self.localized_strings.get("common.languages")) From 42a10a6059524c439a4c524111a595f13db097b7 Mon Sep 17 00:00:00 2001 From: scott-c Date: Mon, 29 Jun 2020 19:18:19 +0800 Subject: [PATCH 31/71] Add ParticleEmitter Component --- common/src/comp/ability.rs | 6 +- common/src/comp/inventory/item/tool.rs | 140 ++++++++++++++----------- common/src/comp/mod.rs | 2 +- common/src/comp/visual.rs | 34 ++++++ common/src/event.rs | 1 + common/src/msg/ecs_packet.rs | 7 ++ common/src/state.rs | 1 + common/src/states/basic_ranged.rs | 7 +- server/src/cmd.rs | 1 + server/src/events/entity_creation.rs | 9 +- server/src/events/mod.rs | 3 +- server/src/sys/sentinel.rs | 22 +++- 12 files changed, 161 insertions(+), 72 deletions(-) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 79292b797e..e37e6bf26d 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -2,7 +2,8 @@ use crate::{ comp::{ ability::Stage, item::{armor::Protection, Item, ItemKind}, - Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile, StateUpdate, + Body, CharacterState, EnergySource, Gravity, LightEmitter, ParticleEmitter, Projectile, + StateUpdate, }, states::{triple_strike::*, *}, sys::character_behavior::JoinData, @@ -61,6 +62,7 @@ pub enum CharacterAbility { projectile: Projectile, projectile_body: Body, projectile_light: Option, + projectile_particles: Option, projectile_gravity: Option, }, Boost { @@ -247,6 +249,7 @@ impl From<&CharacterAbility> for CharacterState { projectile, projectile_body, projectile_light, + projectile_particles, projectile_gravity, energy_cost: _, } => CharacterState::BasicRanged(basic_ranged::Data { @@ -258,6 +261,7 @@ impl From<&CharacterAbility> for CharacterState { projectile: projectile.clone(), projectile_body: *projectile_body, projectile_light: *projectile_light, + projectile_particles: *projectile_particles, projectile_gravity: *projectile_gravity, }), CharacterAbility::Boost { duration, only_up } => CharacterState::Boost(boost::Data { diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 401f9925c7..d7b4524243 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -2,7 +2,8 @@ // version in voxygen\src\meta.rs in order to reset save files to being empty use crate::comp::{ - body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile, + body::object, projectile, Body, CharacterAbility, Gravity, HealthChange, HealthSource, + LightEmitter, Projectile, ParticleEmitter, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -179,6 +180,7 @@ impl Tool { }, projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, + projectile_particles: None, projectile_gravity: Some(Gravity(0.2)), }, ChargedRanged { @@ -193,6 +195,8 @@ impl Tool { recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, + projectile_particles: None, + projectile_gravity: Some(Gravity(0.05)), }, ], Dagger(_) => vec![ @@ -261,40 +265,68 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), - projectile_gravity: None, - }, - BasicRanged { - energy_cost: 400, - holdable: true, - prepare_duration: Duration::from_millis(800), - recover_duration: Duration::from_millis(50), - projectile: Projectile { - hit_solid: vec![ - projectile::Effect::Explode { - power: 1.4 * self.base_power(), - }, - projectile::Effect::Vanish, - ], - hit_entity: vec![ - projectile::Effect::Explode { - power: 1.4 * self.base_power(), - }, - projectile::Effect::Vanish, - ], - time_left: Duration::from_secs(20), - owner: None, - }, - projectile_body: Body::Object(object::Body::BoltFireBig), - projectile_light: Some(LightEmitter { - col: (1.0, 0.75, 0.11).into(), - ..Default::default() - }), - - projectile_gravity: None, - }, - ] - } - }, + projectile::Effect::RewardEnergy(150), + projectile::Effect::Vanish, + ], + time_left: Duration::from_secs(20), + owner: None, + }, + projectile_body: Body::Object(object::Body::BoltFire), + projectile_light: Some(LightEmitter { + col: (0.85, 0.5, 0.11).into(), + ..Default::default() + }), + projectile_particles: Some(ParticleEmitter { + mode: 0, + }), + projectile_gravity: None, + }, + BasicRanged { + energy_cost: 400, + holdable: true, + prepare_duration: Duration::from_millis(800), + recover_duration: Duration::from_millis(50), + projectile: Projectile { + hit_solid: vec![ + projectile::Effect::Explode { power: 1.4 }, + projectile::Effect::Vanish, + ], + hit_entity: vec![ + projectile::Effect::Explode { power: 1.4 }, + projectile::Effect::Vanish, + ], + time_left: Duration::from_secs(20), + owner: None, + }, + projectile_body: Body::Object(object::Body::BoltFireBig), + projectile_light: Some(LightEmitter { + col: (1.0, 0.75, 0.11).into(), + ..Default::default() + }), + projectile_particles: Some(ParticleEmitter { + mode: 0, + }), + projectile_gravity: None, + }, + ], + Staff(StaffKind::Sceptre) => vec![ + BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(0), + recover_duration: Duration::from_millis(300), + base_healthchange: -1, + range: 10.0, + max_angle: 45.0, + }, + BasicMelee { + energy_cost: 350, + buildup_duration: Duration::from_millis(0), + recover_duration: Duration::from_millis(1000), + base_healthchange: 15, + range: 10.0, + max_angle: 45.0, + }, + ], Shield(_) => vec![ BasicMelee { energy_cost: 0, @@ -313,35 +345,15 @@ impl Tool { duration: Duration::from_millis(50), only_up: false, }, - CharacterAbility::Boost { - duration: Duration::from_millis(50), - only_up: true, - }, - BasicRanged { - energy_cost: 0, - holdable: false, - prepare_duration: Duration::from_millis(0), - recover_duration: Duration::from_millis(10), - projectile: Projectile { - hit_solid: vec![projectile::Effect::Stick], - hit_entity: vec![ - projectile::Effect::Stick, - projectile::Effect::Possess, - ], - time_left: Duration::from_secs(10), - owner: None, - }, - projectile_body: Body::Object(object::Body::ArrowSnake), - projectile_light: Some(LightEmitter { - col: (0.0, 1.0, 0.33).into(), - ..Default::default() - }), - projectile_gravity: None, - }, - ] - } else { - vec![] - } + projectile_body: Body::Object(object::Body::ArrowSnake), + projectile_light: Some(LightEmitter { + col: (0.0, 1.0, 0.33).into(), + ..Default::default() + }), + projectile_particles: None, + projectile_gravity: None, + }, + ], }, Empty => vec![BasicMelee { energy_cost: 0, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index ccef80dc8b..a17da95189 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -53,4 +53,4 @@ pub use player::{Player, MAX_MOUNT_RANGE_SQR}; pub use projectile::Projectile; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; pub use stats::{Exp, HealthChange, HealthSource, Level, Stats}; -pub use visual::{LightAnimation, LightEmitter}; +pub use visual::{LightAnimation, LightEmitter, ParticleEmitter}; diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index 013f27aa2c..b1d0f29cae 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -46,3 +46,37 @@ impl Default for LightAnimation { impl Component for LightAnimation { type Storage = FlaggedStorage>; } + + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ParticleEmitter { + + /// Mode 1: sprinkler (inital_velocity, lifespan) + /// Mode 2: smoke (initial_position, boyancy_const, wind, lifespan) + pub mode: u8, // enum? + + // pub vertices: Vec, + // pub texture: RasterFooBar, + + // // mode 1 -- sprinkler. + // pub initial_position: [i8; 3], + // pub initial_velocity: [i8; 3], + // pub lifespan: u32, // in ticks? + + // // mode 2 -- smoke + // pub initial_position: [i8; 3], + // pub boyancy_const: [i8; 3], + // pub wind_sway: [i8; 3], +} + +impl Default for ParticleEmitter { + fn default() -> Self { + Self { + mode: 0, + } + } +} + +impl Component for ParticleEmitter { + type Storage = FlaggedStorage>; +} \ No newline at end of file diff --git a/common/src/event.rs b/common/src/event.rs index 943757a2a7..f291971c25 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -43,6 +43,7 @@ pub enum ServerEvent { dir: Dir, body: comp::Body, light: Option, + particles: Option, projectile: comp::Projectile, gravity: Option, }, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index 726d67321a..e8fa12c33d 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -15,6 +15,7 @@ sum_type! { Stats(comp::Stats), Energy(comp::Energy), LightEmitter(comp::LightEmitter), + ParticleEmitter(comp::ParticleEmitter), Item(comp::Item), Scale(comp::Scale), Group(comp::Group), @@ -42,6 +43,7 @@ sum_type! { Stats(PhantomData), Energy(PhantomData), LightEmitter(PhantomData), + ParticleEmitter(PhantomData), Item(PhantomData), Scale(PhantomData), Group(PhantomData), @@ -69,6 +71,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(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::ParticleEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world), @@ -94,6 +97,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(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::ParticleEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world), @@ -121,6 +125,9 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::(entity, world) }, + EcsCompPhantom::ParticleEmitter(_) => { + sync::handle_remove::(entity, world) + }, EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Group(_) => sync::handle_remove::(entity, world), diff --git a/common/src/state.rs b/common/src/state.rs index 3d003b7a4f..c4798c5813 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -113,6 +113,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs index b41bd6c208..647b166230 100644 --- a/common/src/states/basic_ranged.rs +++ b/common/src/states/basic_ranged.rs @@ -1,5 +1,5 @@ use crate::{ - comp::{Body, CharacterState, Gravity, LightEmitter, Projectile, StateUpdate}, + comp::{Body, CharacterState, Gravity, LightEmitter, ParticleEmitter, Projectile, StateUpdate}, event::ServerEvent, states::utils::*, sys::character_behavior::*, @@ -20,6 +20,7 @@ pub struct Data { pub projectile: Projectile, pub projectile_body: Body, pub projectile_light: Option, + pub projectile_particles: Option, pub projectile_gravity: Option, /// Whether the attack fired already pub exhausted: bool, @@ -48,6 +49,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_particles: self.projectile_particles, projectile_gravity: self.projectile_gravity, exhausted: false, }); @@ -61,6 +63,7 @@ impl CharacterBehavior for Data { body: self.projectile_body, projectile, light: self.projectile_light, + particles: self.projectile_particles, gravity: self.projectile_gravity, }); @@ -72,6 +75,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_particles: self.projectile_particles, projectile_gravity: self.projectile_gravity, exhausted: true, }); @@ -88,6 +92,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_particles: self.projectile_particles, projectile_gravity: self.projectile_gravity, exhausted: true, }); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b580cfbd98..14c7b39338 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -910,6 +910,7 @@ fn handle_light( .create_entity_synced() .with(pos) .with(comp::ForceUpdate) + .with(comp::ParticleEmitter { mode: 0 }) .with(light_emitter); if let Some(light_offset) = light_offset_opt { builder.with(light_offset).build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index b2924d9d15..d2f3b9d774 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -1,8 +1,8 @@ use crate::{sys, Server, StateExt}; use common::{ comp::{ - self, group, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, - Projectile, Scale, Stats, Vel, WaypointArea, + self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, + ParticleEmitter, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, util::Dir, }; @@ -77,6 +77,7 @@ pub fn handle_shoot( dir: Dir, body: Body, light: Option, + particles: Option, projectile: Projectile, gravity: Option, ) { @@ -96,6 +97,9 @@ pub fn handle_shoot( if let Some(light) = light { builder = builder.with(light) } + if let Some(particles) = particles { + builder = builder.with(particles) + } if let Some(gravity) = gravity { builder = builder.with(gravity) } @@ -113,6 +117,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { flicker: 1.0, animated: true, }) + .with(ParticleEmitter { mode: 0 }) .with(WaypointArea::default()) .build(); } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 412d9a536a..9761e1d869 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -61,9 +61,10 @@ impl Server { dir, body, light, + particles, projectile, gravity, - } => handle_shoot(self, entity, dir, body, light, projectile, gravity), + } => handle_shoot(self, entity, dir, body, light, particles, projectile, gravity), ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change), ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index adcc76bd30..0fe4af0a3d 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,8 +1,9 @@ use super::SysTimer; use common::{ comp::{ - Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, - Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, + Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, + Loadout, Mass, MountState, Mounting, Ori, ParticleEmitter, Player, Pos, Scale, Stats, + Sticky, Vel, }, msg::EcsCompPacket, sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt}, @@ -44,6 +45,7 @@ pub struct TrackedComps<'a> { pub energy: ReadStorage<'a, Energy>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, + pub particle_emitter: ReadStorage<'a, ParticleEmitter>, pub item: ReadStorage<'a, Item>, pub scale: ReadStorage<'a, Scale>, pub mounting: ReadStorage<'a, Mounting>, @@ -92,6 +94,10 @@ impl<'a> TrackedComps<'a> { .get(entity) .copied() .map(|c| comps.push(c.into())); + self.particle_emitter + .get(entity) + .copied() + .map(|c| comps.push(c.into())); self.item.get(entity).cloned().map(|c| comps.push(c.into())); self.scale .get(entity) @@ -147,6 +153,7 @@ pub struct ReadTrackers<'a> { pub energy: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, + pub particle_emitter: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, pub scale: ReadExpect<'a, UpdateTracker>, pub mounting: ReadExpect<'a, UpdateTracker>, @@ -180,6 +187,12 @@ impl<'a> ReadTrackers<'a> { &comps.light_emitter, filter, ) + .with_component( + &comps.uid, + &*self.particle_emitter, + &comps.particle_emitter, + filter, + ) .with_component(&comps.uid, &*self.item, &comps.item, filter) .with_component(&comps.uid, &*self.scale, &comps.scale, filter) .with_component(&comps.uid, &*self.mounting, &comps.mounting, filter) @@ -210,6 +223,7 @@ pub struct WriteTrackers<'a> { energy: WriteExpect<'a, UpdateTracker>, can_build: WriteExpect<'a, UpdateTracker>, light_emitter: WriteExpect<'a, UpdateTracker>, + particle_emitter: WriteExpect<'a, UpdateTracker>, item: WriteExpect<'a, UpdateTracker>, scale: WriteExpect<'a, UpdateTracker>, mounting: WriteExpect<'a, UpdateTracker>, @@ -232,6 +246,9 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.energy.record_changes(&comps.energy); trackers.can_build.record_changes(&comps.can_build); trackers.light_emitter.record_changes(&comps.light_emitter); + trackers + .particle_emitter + .record_changes(&comps.particle_emitter); trackers.item.record_changes(&comps.item); trackers.scale.record_changes(&comps.scale); trackers.mounting.record_changes(&comps.mounting); @@ -287,6 +304,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); From 7e35617f591fbb014694f81445698150b2496647 Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 30 Jun 2020 22:29:35 +0800 Subject: [PATCH 32/71] Add particle pipeline --- assets/voxygen/shaders/include/globals.glsl | 1 + assets/voxygen/shaders/particle-frag.glsl | 38 +++++++++ assets/voxygen/shaders/particle-vert.glsl | 62 ++++++++++++++ voxygen/src/hud/mod.rs | 1 + voxygen/src/hud/settings_window.rs | 3 + voxygen/src/render/pipelines/mod.rs | 5 ++ voxygen/src/render/pipelines/particle.rs | 90 +++++++++++++++++++++ voxygen/src/render/renderer.rs | 52 +++++++++++- voxygen/src/scene/mod.rs | 2 + voxygen/src/scene/simple.rs | 1 + voxygen/src/session.rs | 9 ++- voxygen/src/settings.rs | 2 + 12 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 assets/voxygen/shaders/particle-frag.glsl create mode 100644 assets/voxygen/shaders/particle-vert.glsl create mode 100644 voxygen/src/render/pipelines/particle.rs diff --git a/assets/voxygen/shaders/include/globals.glsl b/assets/voxygen/shaders/include/globals.glsl index 895492a23a..7436e5c036 100644 --- a/assets/voxygen/shaders/include/globals.glsl +++ b/assets/voxygen/shaders/include/globals.glsl @@ -17,6 +17,7 @@ uniform u_globals { // 1 - ThirdPerson uint cam_mode; float sprite_render_distance; + float particle_render_distance; }; // Specifies the pattern used in the player dithering diff --git a/assets/voxygen/shaders/particle-frag.glsl b/assets/voxygen/shaders/particle-frag.glsl new file mode 100644 index 0000000000..3e2ef6b9e6 --- /dev/null +++ b/assets/voxygen/shaders/particle-frag.glsl @@ -0,0 +1,38 @@ +#version 330 core + +#include + +in vec3 f_pos; +flat in vec3 f_norm; +in vec3 f_col; +in float f_ao; +in float f_light; + +out vec4 tgt_color; + +#include +#include + +const float FADE_DIST = 32.0; + +void main() { + vec3 light, diffuse_light, ambient_light; + get_sun_diffuse(f_norm, time_of_day.x, light, diffuse_light, ambient_light, 1.0); + float point_shadow = shadow_at(f_pos, f_norm); + diffuse_light *= f_light * point_shadow; + ambient_light *= f_light, point_shadow; + vec3 point_light = light_at(f_pos, f_norm); + light += point_light; + diffuse_light += point_light; + float ao = pow(f_ao, 0.5) * 0.85 + 0.15; + ambient_light *= ao; + diffuse_light *= ao; + vec3 surf_color = illuminate(f_col, light, diffuse_light, ambient_light); + + float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); + vec4 clouds; + vec3 fog_color = get_sky_color(normalize(f_pos - cam_pos.xyz), time_of_day.x, cam_pos.xyz, f_pos, 0.5, true, clouds); + vec3 color = mix(mix(surf_color, fog_color, fog_level), clouds.rgb, clouds.a); + + tgt_color = vec4(color, 1.0 - clamp((distance(focus_pos.xy, f_pos.xy) - (particle_render_distance - FADE_DIST)) / FADE_DIST, 0, 1)); +} diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl new file mode 100644 index 0000000000..86f00c3aad --- /dev/null +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -0,0 +1,62 @@ +#version 330 core + +#include +#include + +in vec3 v_pos; +in uint v_col; +in uint v_norm_ao; +in vec4 inst_mat0; +in vec4 inst_mat1; +in vec4 inst_mat2; +in vec4 inst_mat3; +in vec3 inst_col; +in float inst_wind_sway; + +out vec3 f_pos; +flat out vec3 f_norm; +out vec3 f_col; +out float f_ao; +out float f_light; + +const float SCALE = 1.0 / 11.0; + +void main() { + mat4 inst_mat; + inst_mat[0] = inst_mat0; + inst_mat[1] = inst_mat1; + inst_mat[2] = inst_mat2; + inst_mat[3] = inst_mat3; + + vec3 particle_pos = (inst_mat * vec4(0, 0, 0, 1)).xyz; + + f_pos = (inst_mat * vec4(v_pos * SCALE, 1)).xyz; + f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); + + // Wind waving + f_pos += inst_wind_sway * vec3( + sin(tick.x * 1.5 + f_pos.y * 0.1) * sin(tick.x * 0.35), + sin(tick.x * 1.5 + f_pos.x * 0.1) * sin(tick.x * 0.25), + 0.0 + ) * pow(abs(v_pos.z) * SCALE, 1.3) * 0.2; + + // First 3 normals are negative, next 3 are positive + vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); + f_norm = (inst_mat * vec4(normals[(v_norm_ao >> 0) & 0x7u], 0)).xyz; + + vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; + f_col = srgb_to_linear(col) * srgb_to_linear(inst_col); + f_ao = float((v_norm_ao >> 3) & 0x3u) / 4.0; + + // Select glowing + if (select_pos.w > 0 && select_pos.xyz == floor(particle_pos)) { + f_col *= 4.0; + } + + f_light = 1.0; + + gl_Position = + all_mat * + vec4(f_pos, 1); + gl_Position.z = -1000.0 / (gl_Position.z + 10000.0); +} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 33d400735e..7f8242af23 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -266,6 +266,7 @@ pub enum Event { ToggleSmoothPan(bool), AdjustViewDistance(u32), AdjustSpriteRenderDistance(u32), + AdjustParticleRenderDistance(u32), AdjustFigureLoDRenderDistance(u32), AdjustMusicVolume(f32), AdjustSfxVolume(f32), diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 142b4d0984..c0b8e42fff 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -1845,6 +1845,7 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.sprite_dist_text, ui); + Text::new(&format!( "{}", self.global_state.settings.graphics.sprite_render_distance @@ -1898,6 +1899,8 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.figure_dist_value, ui); + // TODO: Particle View Distance slider. + // AaMode Text::new(&self.localized_strings.get("hud.settings.antialiasing_mode")) .down_from(state.ids.gamma_slider, 8.0) diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index 81ef76456f..a3e978a70c 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -3,6 +3,7 @@ pub mod fluid; pub mod postprocess; pub mod skybox; pub mod sprite; +pub mod particle; pub mod terrain; pub mod ui; @@ -29,6 +30,7 @@ gfx_defines! { gamma: [f32; 4] = "gamma", cam_mode: u32 = "cam_mode", sprite_render_distance: f32 = "sprite_render_distance", + particle_render_distance: f32 = "particle_render_distance", } constant Light { @@ -61,6 +63,7 @@ impl Globals { gamma: f32, cam_mode: CameraMode, sprite_render_distance: f32, + particle_render_distance: f32, ) -> Self { Self { view_mat: view_mat.into_col_arrays(), @@ -81,6 +84,7 @@ impl Globals { gamma: [gamma; 4], cam_mode: cam_mode as u32, sprite_render_distance, + particle_render_distance, } } } @@ -103,6 +107,7 @@ impl Default for Globals { 1.0, CameraMode::ThirdPerson, 250.0, + 250.0, ) } } diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs new file mode 100644 index 0000000000..89fde89543 --- /dev/null +++ b/voxygen/src/render/pipelines/particle.rs @@ -0,0 +1,90 @@ +use super::{ + super::{Pipeline, TgtColorFmt, TgtDepthStencilFmt}, + Globals, Light, Shadow, +}; +use gfx::{ + self, gfx_defines, gfx_impl_struct_meta, gfx_pipeline, gfx_pipeline_inner, + gfx_vertex_struct_meta, + state::{ColorMask, Comparison, Stencil, StencilOp}, +}; +use vek::*; + +gfx_defines! { + vertex Vertex { + pos: [f32; 3] = "v_pos", + // ____BBBBBBBBGGGGGGGGRRRRRRRR + col: u32 = "v_col", + // ...AANNN + // A = AO + // N = Normal + norm_ao: u32 = "v_norm_ao", + } + + vertex Instance { + inst_mat0: [f32; 4] = "inst_mat0", + inst_mat1: [f32; 4] = "inst_mat1", + inst_mat2: [f32; 4] = "inst_mat2", + inst_mat3: [f32; 4] = "inst_mat3", + inst_col: [f32; 3] = "inst_col", + inst_wind_sway: f32 = "inst_wind_sway", + } + + pipeline pipe { + vbuf: gfx::VertexBuffer = (), + ibuf: gfx::InstanceBuffer = (), + + globals: gfx::ConstantBuffer = "u_globals", + lights: gfx::ConstantBuffer = "u_lights", + shadows: gfx::ConstantBuffer = "u_shadows", + + noise: gfx::TextureSampler = "t_noise", + + tgt_color: gfx::BlendTarget = ("tgt_color", ColorMask::all(), gfx::preset::blend::ALPHA), + tgt_depth_stencil: gfx::DepthStencilTarget = (gfx::preset::depth::LESS_EQUAL_WRITE,Stencil::new(Comparison::Always,0xff,(StencilOp::Keep,StencilOp::Keep,StencilOp::Keep))), + } +} + +impl Vertex { + #[allow(clippy::collapsible_if)] + pub fn new(pos: Vec3, norm: Vec3, col: Rgb, ao: f32) -> Self { + let norm_bits = if norm.x != 0.0 { + if norm.x < 0.0 { 0 } else { 1 } + } else if norm.y != 0.0 { + if norm.y < 0.0 { 2 } else { 3 } + } else { + if norm.z < 0.0 { 4 } else { 5 } + }; + + Self { + pos: pos.into_array(), + col: col + .map2(Rgb::new(0, 8, 16), |e, shift| ((e * 255.0) as u32) << shift) + .reduce_bitor(), + norm_ao: norm_bits | (((ao * 3.9999) as u32) << 3), + } + } +} + +impl Instance { + pub fn new(mat: Mat4, col: Rgb, wind_sway: f32) -> Self { + let mat_arr = mat.into_col_arrays(); + Self { + inst_mat0: mat_arr[0], + inst_mat1: mat_arr[1], + inst_mat2: mat_arr[2], + inst_mat3: mat_arr[3], + inst_col: col.into_array(), + inst_wind_sway: wind_sway, + } + } +} + +impl Default for Instance { + fn default() -> Self { Self::new(Mat4::identity(), Rgb::broadcast(1.0), 0.0) } +} + +pub struct ParticlePipeline; + +impl Pipeline for ParticlePipeline { + type Vertex = Vertex; +} diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index a6e37ff192..80bdb65ba5 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -4,7 +4,7 @@ use super::{ instances::Instances, mesh::Mesh, model::{DynamicModel, Model}, - pipelines::{figure, fluid, postprocess, skybox, sprite, terrain, ui, Globals, Light, Shadow}, + pipelines::{figure, fluid, postprocess, skybox, sprite, particle, terrain, ui, Globals, Light, Shadow}, texture::Texture, AaMode, CloudMode, FluidMode, Pipeline, RenderError, }; @@ -70,6 +70,7 @@ pub struct Renderer { terrain_pipeline: GfxPipeline>, fluid_pipeline: GfxPipeline>, sprite_pipeline: GfxPipeline>, + particle_pipeline: GfxPipeline>, ui_pipeline: GfxPipeline>, postprocess_pipeline: GfxPipeline>, player_shadow_pipeline: GfxPipeline>, @@ -103,6 +104,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -146,6 +148,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -341,6 +344,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -350,6 +354,7 @@ impl Renderer { self.terrain_pipeline = terrain_pipeline; self.fluid_pipeline = fluid_pipeline; self.sprite_pipeline = sprite_pipeline; + self.particle_pipeline = particle_pipeline; self.ui_pipeline = ui_pipeline; self.postprocess_pipeline = postprocess_pipeline; self.player_shadow_pipeline = player_shadow_pipeline; @@ -711,6 +716,37 @@ impl Renderer { ); } + /// Queue the rendering of the provided particle in the upcoming frame. + pub fn render_particles( + &mut self, + model: &Model, + globals: &Consts, + instances: &Instances, + lights: &Consts, + shadows: &Consts, + ) { + self.encoder.draw( + &gfx::Slice { + start: model.vertex_range().start, + end: model.vertex_range().end, + base_vertex: 0, + instances: Some((instances.count() as u32, 0)), + buffer: gfx::IndexBuffer::Auto, + }, + &self.particle_pipeline.pso, + &particle::pipe::Data { + vbuf: model.vbuf.clone(), + ibuf: instances.ibuf.clone(), + globals: globals.buf.clone(), + lights: lights.buf.clone(), + shadows: shadows.buf.clone(), + noise: (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()), + tgt_color: self.tgt_color_view.clone(), + tgt_depth_stencil: (self.tgt_depth_stencil_view.clone(), (1, 1)), + }, + ); + } + /// Queue the rendering of the provided UI element in the upcoming frame. pub fn render_ui_element( &mut self, @@ -793,6 +829,7 @@ fn create_pipelines( GfxPipeline>, GfxPipeline>, GfxPipeline>, + GfxPipeline>, GfxPipeline>, GfxPipeline>, GfxPipeline>, @@ -914,6 +951,18 @@ fn create_pipelines( gfx::state::CullFace::Back, )?; + // Construct a pipeline for rendering particles + let particle_pipeline = create_pipeline( + factory, + particle::pipe::new(), + &assets::load_watched::("voxygen.shaders.particle-vert", shader_reload_indicator) + .unwrap(), + &assets::load_watched::("voxygen.shaders.particle-frag", shader_reload_indicator) + .unwrap(), + &include_ctx, + gfx::state::CullFace::Back, + )?; + // Construct a pipeline for rendering UI elements let ui_pipeline = create_pipeline( factory, @@ -975,6 +1024,7 @@ fn create_pipelines( terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 4c18a92207..19b6144760 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -78,6 +78,7 @@ pub struct SceneData<'a> { pub gamma: f32, pub mouse_smoothing: bool, pub sprite_render_distance: f32, + pub particle_render_distance: f32, pub figure_lod_render_distance: f32, pub is_aiming: bool, } @@ -369,6 +370,7 @@ impl Scene { scene_data.gamma, self.camera.get_mode(), scene_data.sprite_render_distance as f32 - 20.0, + scene_data.particle_render_distance as f32 - 20.0, )]) .expect("Failed to update global constants"); diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index c89ebc6511..fb620aa0f1 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -190,6 +190,7 @@ impl Scene { scene_data.gamma, self.camera.get_mode(), 250.0, + 250.0, )]) { error!(?e, "Renderer failed to update"); } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 3c5f266164..fb44b952a4 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -785,6 +785,11 @@ impl PlayState for SessionState { sprite_render_distance; global_state.settings.save_to_file_warn(); }, + HudEvent::AdjustParticleRenderDistance(particle_render_distance) => { + global_state.settings.graphics.particle_render_distance = + particle_render_distance; + global_state.settings.save_to_file_warn(); + }, HudEvent::AdjustFigureLoDRenderDistance(figure_lod_render_distance) => { global_state.settings.graphics.figure_lod_render_distance = figure_lod_render_distance; @@ -997,7 +1002,9 @@ impl PlayState for SessionState { gamma: global_state.settings.graphics.gamma, mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable, sprite_render_distance: global_state.settings.graphics.sprite_render_distance - as f32, + as f32, + particle_render_distance: global_state.settings.graphics.particle_render_distance + as f32, figure_lod_render_distance: global_state .settings .graphics diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 441108bdb7..ef1184c8ee 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -603,6 +603,7 @@ impl Default for Log { pub struct GraphicsSettings { pub view_distance: u32, pub sprite_render_distance: u32, + pub particle_render_distance: u32, pub figure_lod_render_distance: u32, pub max_fps: u32, pub fov: u16, @@ -619,6 +620,7 @@ impl Default for GraphicsSettings { Self { view_distance: 10, sprite_render_distance: 150, + particle_render_distance: 150, figure_lod_render_distance: 250, max_fps: 60, fov: 50, From 39b676cd8f436555ec79424cea5f5f4637cf6c53 Mon Sep 17 00:00:00 2001 From: scott-c Date: Sun, 5 Jul 2020 20:10:58 +0800 Subject: [PATCH 33/71] Add ParticleMgr --- assets/voxygen/shaders/particle-vert.glsl | 15 +- common/src/comp/inventory/item/tool.rs | 8 +- common/src/comp/mod.rs | 2 +- common/src/comp/visual.rs | 27 +--- server/src/cmd.rs | 5 +- server/src/events/entity_creation.rs | 5 +- voxygen/src/mesh/segment.rs | 70 ++++++++- voxygen/src/render/mod.rs | 1 + voxygen/src/render/pipelines/particle.rs | 29 +++- voxygen/src/scene/mod.rs | 25 ++++ voxygen/src/scene/particle.rs | 170 ++++++++++++++++++++++ 11 files changed, 322 insertions(+), 35 deletions(-) create mode 100644 voxygen/src/scene/particle.rs diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 86f00c3aad..a50cc9a6a9 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -11,6 +11,8 @@ in vec4 inst_mat1; in vec4 inst_mat2; in vec4 inst_mat3; in vec3 inst_col; +in vec3 inst_vel; +in vec4 inst_tick; in float inst_wind_sway; out vec3 f_pos; @@ -34,11 +36,14 @@ void main() { f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); // Wind waving - f_pos += inst_wind_sway * vec3( - sin(tick.x * 1.5 + f_pos.y * 0.1) * sin(tick.x * 0.35), - sin(tick.x * 1.5 + f_pos.x * 0.1) * sin(tick.x * 0.25), - 0.0 - ) * pow(abs(v_pos.z) * SCALE, 1.3) * 0.2; + //f_pos += inst_wind_sway * vec3( + // sin(tick.x * 1.5 + f_pos.y * 0.1) * sin(tick.x * 0.35), + // sin(tick.x * 1.5 + f_pos.x * 0.1) * sin(tick.x * 0.25), + // 0.0 + //) * pow(abs(v_pos.z) * SCALE, 1.3) * 0.2; + + float elapsed = (tick.x - inst_tick.x) / 100000.0; + f_pos += (inst_vel * elapsed); // First 3 normals are negative, next 3 are positive vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index d7b4524243..14a1c37d41 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -2,8 +2,8 @@ // version in voxygen\src\meta.rs in order to reset save files to being empty use crate::comp::{ - body::object, projectile, Body, CharacterAbility, Gravity, HealthChange, HealthSource, - LightEmitter, Projectile, ParticleEmitter, + body::object, projectile, visual::ParticleEmitterMode, Body, CharacterAbility, Gravity, + HealthChange, HealthSource, LightEmitter, ParticleEmitter, Projectile, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -277,7 +277,7 @@ impl Tool { ..Default::default() }), projectile_particles: Some(ParticleEmitter { - mode: 0, + mode: ParticleEmitterMode::Sprinkler, }), projectile_gravity: None, }, @@ -304,7 +304,7 @@ impl Tool { ..Default::default() }), projectile_particles: Some(ParticleEmitter { - mode: 0, + mode: ParticleEmitterMode::Sprinkler, }), projectile_gravity: None, }, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index a17da95189..4126c1a9df 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -18,7 +18,7 @@ mod player; pub mod projectile; pub mod skills; mod stats; -mod visual; +pub mod visual; // Reexports pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index b1d0f29cae..215ce17959 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -46,37 +46,24 @@ impl Default for LightAnimation { impl Component for LightAnimation { type Storage = FlaggedStorage>; } - +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ParticleEmitterMode { + Sprinkler, +} #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ParticleEmitter { - - /// Mode 1: sprinkler (inital_velocity, lifespan) - /// Mode 2: smoke (initial_position, boyancy_const, wind, lifespan) - pub mode: u8, // enum? - - // pub vertices: Vec, - // pub texture: RasterFooBar, - - // // mode 1 -- sprinkler. - // pub initial_position: [i8; 3], - // pub initial_velocity: [i8; 3], - // pub lifespan: u32, // in ticks? - - // // mode 2 -- smoke - // pub initial_position: [i8; 3], - // pub boyancy_const: [i8; 3], - // pub wind_sway: [i8; 3], + pub mode: ParticleEmitterMode, } impl Default for ParticleEmitter { fn default() -> Self { Self { - mode: 0, + mode: ParticleEmitterMode::Sprinkler, } } } impl Component for ParticleEmitter { type Storage = FlaggedStorage>; -} \ No newline at end of file +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 14c7b39338..090e587a40 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -25,6 +25,7 @@ use world::util::Sampler; use scan_fmt::{scan_fmt, scan_fmt_some}; use tracing::error; +use comp::visual::ParticleEmitterMode; pub trait ChatCommandExt { fn execute(&self, server: &mut Server, entity: EcsEntity, args: String); @@ -910,7 +911,9 @@ fn handle_light( .create_entity_synced() .with(pos) .with(comp::ForceUpdate) - .with(comp::ParticleEmitter { mode: 0 }) + .with(comp::ParticleEmitter { + mode: ParticleEmitterMode::Sprinkler, + }) .with(light_emitter); if let Some(light_offset) = light_offset_opt { builder.with(light_offset).build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index d2f3b9d774..fd25760dce 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -6,6 +6,7 @@ use common::{ }, util::Dir, }; +use comp::visual::ParticleEmitterMode; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -117,7 +118,9 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { flicker: 1.0, animated: true, }) - .with(ParticleEmitter { mode: 0 }) + .with(ParticleEmitter { + mode: ParticleEmitterMode::Sprinkler, + }) .with(WaypointArea::default()) .build(); } diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs index c69d348b45..2909c51f35 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -1,6 +1,6 @@ use crate::{ mesh::{vol, Meshable}, - render::{self, FigurePipeline, Mesh, SpritePipeline}, + render::{self, FigurePipeline, Mesh, ParticlePipeline, SpritePipeline}, }; use common::{ figure::Cell, @@ -11,6 +11,7 @@ use vek::*; type FigureVertex = ::Vertex; type SpriteVertex = ::Vertex; +type ParticleVertex = ::Vertex; impl<'a, V: 'a> Meshable<'a, FigurePipeline, FigurePipeline> for V where @@ -147,6 +148,73 @@ where } } +impl<'a, V: 'a> Meshable<'a, ParticlePipeline, ParticlePipeline> for V +where + V: BaseVol + ReadVol + SizedVol, + /* TODO: Use VolIterator instead of manually iterating + * &'a V: IntoVolIterator<'a> + IntoFullVolIterator<'a>, + * &'a V: BaseVol, */ +{ + type Pipeline = ParticlePipeline; + type Supplement = (Vec3, Vec3); + type TranslucentPipeline = ParticlePipeline; + + #[allow(clippy::needless_range_loop)] // TODO: Pending review in #587 + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + fn generate_mesh( + &'a self, + (offs, scale): Self::Supplement, + ) -> (Mesh, Mesh) { + let mut mesh = Mesh::new(); + + let vol_iter = (self.lower_bound().x..self.upper_bound().x) + .map(|i| { + (self.lower_bound().y..self.upper_bound().y).map(move |j| { + (self.lower_bound().z..self.upper_bound().z).map(move |k| Vec3::new(i, j, k)) + }) + }) + .flatten() + .flatten() + .map(|pos| (pos, self.get(pos).map(|x| *x).unwrap_or(Vox::empty()))); + + for (pos, vox) in vol_iter { + if let Some(col) = vox.get_color() { + vol::push_vox_verts( + &mut mesh, + faces_to_make(self, pos, true, |vox| vox.is_empty()), + offs + pos.map(|e| e as f32), + &[[[Rgba::from_opaque(col); 3]; 3]; 3], + |origin, norm, col, light, ao| { + ParticleVertex::new( + origin * scale, + norm, + linear_to_srgb(srgb_to_linear(col) * light), + ao, + ) + }, + &{ + let mut ls = [[[None; 3]; 3]; 3]; + for x in 0..3 { + for y in 0..3 { + for z in 0..3 { + ls[z][y][x] = self + .get(pos + Vec3::new(x as i32, y as i32, z as i32) - 1) + .map(|v| v.is_empty()) + .unwrap_or(true) + .then_some(1.0); + } + } + } + ls + }, + ); + } + } + + (mesh, Mesh::new()) + } +} + /// Use the 6 voxels/blocks surrounding the one at the specified position /// to detemine which faces should be drawn fn faces_to_make( diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 9bf74d5aa9..408251d9ad 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -22,6 +22,7 @@ pub use self::{ create_mesh as create_pp_mesh, Locals as PostProcessLocals, PostProcessPipeline, }, skybox::{create_mesh as create_skybox_mesh, Locals as SkyboxLocals, SkyboxPipeline}, + particle::{Instance as ParticleInstance, ParticlePipeline}, sprite::{Instance as SpriteInstance, SpritePipeline}, terrain::{Locals as TerrainLocals, TerrainPipeline}, ui::{ diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 89fde89543..1352549054 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -2,6 +2,7 @@ use super::{ super::{Pipeline, TgtColorFmt, TgtDepthStencilFmt}, Globals, Light, Shadow, }; +use common::comp::visual::ParticleEmitterMode; use gfx::{ self, gfx_defines, gfx_impl_struct_meta, gfx_pipeline, gfx_pipeline_inner, gfx_vertex_struct_meta, @@ -26,7 +27,10 @@ gfx_defines! { inst_mat2: [f32; 4] = "inst_mat2", inst_mat3: [f32; 4] = "inst_mat3", inst_col: [f32; 3] = "inst_col", + inst_vel: [f32; 3] = "inst_vel", + inst_tick: [f32; 4] = "inst_tick", inst_wind_sway: f32 = "inst_wind_sway", + mode: u8 = "mode", } pipeline pipe { @@ -66,7 +70,14 @@ impl Vertex { } impl Instance { - pub fn new(mat: Mat4, col: Rgb, wind_sway: f32) -> Self { + pub fn new( + mat: Mat4, + col: Rgb, + vel: Vec3, + tick: u64, + wind_sway: f32, + mode: ParticleEmitterMode, + ) -> Self { let mat_arr = mat.into_col_arrays(); Self { inst_mat0: mat_arr[0], @@ -74,13 +85,27 @@ impl Instance { inst_mat2: mat_arr[2], inst_mat3: mat_arr[3], inst_col: col.into_array(), + inst_vel: vel.into_array(), + inst_tick: [tick as f32; 4], + inst_wind_sway: wind_sway, + + mode: mode as u8, } } } impl Default for Instance { - fn default() -> Self { Self::new(Mat4::identity(), Rgb::broadcast(1.0), 0.0) } + fn default() -> Self { + Self::new( + Mat4::identity(), + Rgb::broadcast(1.0), + Vec3::zero(), + 0, + 0.0, + ParticleEmitterMode::Sprinkler, + ) + } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 19b6144760..9b0f6c3fe5 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1,11 +1,13 @@ pub mod camera; pub mod figure; +pub mod particle; pub mod simple; pub mod terrain; use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, + particle::ParticleMgr, terrain::Terrain, }; use crate::{ @@ -62,6 +64,7 @@ pub struct Scene { loaded_distance: f32, select_pos: Option>, + particle_mgr: ParticleMgr, figure_mgr: FigureMgr, sfx_mgr: SfxMgr, music_mgr: MusicMgr, @@ -113,6 +116,7 @@ impl Scene { loaded_distance: 0.0, select_pos: None, + particle_mgr: ParticleMgr::new(renderer), figure_mgr: FigureMgr::new(), sfx_mgr: SfxMgr::new(), music_mgr: MusicMgr::new(), @@ -128,6 +132,9 @@ impl Scene { /// Get a reference to the scene's terrain. pub fn terrain(&self) -> &Terrain { &self.terrain } + /// Get a reference to the scene's particle manager. + pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr } + /// Get a reference to the scene's figure manager. pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr } @@ -390,6 +397,16 @@ impl Scene { // Remove unused figures. self.figure_mgr.clean(scene_data.tick); + // Maintain the particles. + self.particle_mgr.maintain( + renderer, + &scene_data, + self.camera.get_focus_pos(), + self.loaded_distance, + view_mat, + proj_mat, + ); + // Maintain audio self.sfx_mgr .maintain(audio, scene_data.state, scene_data.player_entity); @@ -425,6 +442,14 @@ impl Scene { scene_data.figure_lod_render_distance, ); + self.particle_mgr.render( + renderer, + &self.globals, + &self.lights, + &self.shadows, + self.camera.get_focus_pos(), + ); + // Render the skybox. renderer.render_skybox(&self.skybox.model, &self.globals, &self.skybox.locals); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs new file mode 100644 index 0000000000..c0cd9cff1d --- /dev/null +++ b/voxygen/src/scene/particle.rs @@ -0,0 +1,170 @@ +use super::SceneData; +use crate::{ + mesh::Meshable, + render::{ + mesh::Quad, Consts, Globals, Instances, Light, Mesh, Model, ParticleInstance, + ParticlePipeline, Renderer, Shadow, + }, +}; +use common::{ + assets, + comp::{visual::ParticleEmitterMode, Ori, ParticleEmitter, Pos, Vel}, + figure::Segment, + vol::BaseVol, +}; +use dot_vox::DotVoxData; +use hashbrown::HashMap; +use rand::Rng; +use specs::{Entity as EcsEntity, Join, WorldExt}; +use tracing::{debug, error, warn}; +use vek::{Mat4, Rgb, Vec3}; +struct Particles { + // this is probably nieve, + // could cache and re-use between particles, + // should be a cache key? + // model: Model, + instances: Instances, +} + +pub struct ParticleMgr { + entity_particles: HashMap>, + model_cache: Model, +} + +impl ParticleMgr { + pub fn new(renderer: &mut Renderer) -> Self { + let offset = Vec3::zero(); + let lod_scale = Vec3::one(); + + // TODO: from cache + let vox = assets::load_expect::("voxygen.voxel.not_found"); + + // TODO: from cache + let mesh = &Meshable::::generate_mesh( + &Segment::from(vox.as_ref()), + (offset * lod_scale, Vec3::one() / lod_scale), + ) + .0; + + // TODO: from cache + let model = renderer + .create_model(mesh) + .expect("Failed to create particle model"); + + Self { + entity_particles: HashMap::new(), + model_cache: model, + } + } + + pub fn maintain( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + focus_pos: Vec3, + loaded_distance: f32, + view_mat: Mat4, + proj_mat: Mat4, + ) { + let state = scene_data.state; + let ecs = state.ecs(); + + let tick = scene_data.tick; + + // remove dead particles + + // remove dead entities, with dead particles + self.entity_particles.retain(|k, v| ecs.is_alive(*k)); + + // add living entities particles + for (_i, (entity, particle_emitter, pos, ori, vel)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), + ) + .join() + .enumerate() + { + let entry = self + .entity_particles + .entry(entity) + .or_insert_with(|| into_particles(renderer, tick, particle_emitter, pos, ori, vel)); + } + } + + pub fn render( + &self, + renderer: &mut Renderer, + globals: &Consts, + lights: &Consts, + shadows: &Consts, + focus_pos: Vec3, + ) { + for particles in self.entity_particles.values() { + for particle in particles { + renderer.render_particles( + &self.model_cache, + globals, + &particle.instances, + lights, + shadows, + ); + } + } + } +} + +fn into_particles( + renderer: &mut Renderer, + tick: u64, + particle_emitter: &ParticleEmitter, + pos: &Pos, + ori: Option<&Ori>, + vel: Option<&Vel>, +) -> Vec { + let mut rng = rand::thread_rng(); + + let desired_instance_count = 100; + + // let ori_default = Ori::default(); + let vel_default = Vel::default(); + + // let ori2 = ori.unwrap_or_else(|| &ori_default); + let vel2 = vel.unwrap_or_else(|| &vel_default).0; + let mut instances_vec = Vec::new(); + + for x in 0..desired_instance_count { + // how does ParticleEmitterMode fit in here? + // can we have a ParticleInstance type per ParticleEmitterMode? + // can we mix and match instance types in the same instances_vec? + instances_vec.push(ParticleInstance::new( + Mat4::identity() + // initial rotation + .rotated_x(rng.gen_range(0.0, 3.14 * 2.0)) + .rotated_y(rng.gen_range(0.0, 3.14 * 2.0)) + .rotated_z(rng.gen_range(0.0, 3.14 * 2.0)) + // inition position + .translated_3d( + pos.0 + + Vec3::new( + rng.gen_range(-5.0, 5.0), + rng.gen_range(-5.0, 5.0), + rng.gen_range(0.0, 10.0), + ), + ), + Rgb::broadcast(1.0), // instance color + vel2 + Vec3::broadcast(rng.gen_range(-5.0, 5.0)), + tick, + rng.gen_range(0.0, 20.0), // wind sway + ParticleEmitterMode::Sprinkler, // particle_emitter.mode */ + )); + } + + let instances = renderer + .create_instances(&instances_vec) + .expect("Failed to upload particle instances to the GPU!"); + + vec![Particles { instances }] +} From da5f4828a5237c88df2a1f7585e9f2887eceef6e Mon Sep 17 00:00:00 2001 From: scott-c Date: Sat, 11 Jul 2020 18:37:19 +0800 Subject: [PATCH 34/71] Add particle lifespan --- common/src/comp/inventory/item/tool.rs | 14 ++- common/src/comp/visual.rs | 22 +++- server/src/cmd.rs | 6 +- server/src/events/entity_creation.rs | 4 +- voxygen/src/scene/particle.rs | 156 ++++++++++++++++--------- 5 files changed, 133 insertions(+), 69 deletions(-) diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 14a1c37d41..fc594bd7df 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -278,6 +278,16 @@ impl Tool { }), projectile_particles: Some(ParticleEmitter { mode: ParticleEmitterMode::Sprinkler, + // model_key: "voxygen.voxel.not_found", + count: (2, 3), + frequency: Duration::from_millis(50), + initial_lifespan: Duration::from_millis(500), + initial_offset: (vek::Vec3::broadcast(-1.0), vek::Vec3::broadcast(1.0)), + initial_orientation: ( + vek::Vec3::broadcast(-1.0), + vek::Vec3::broadcast(1.0), + ), + initial_scale: (0.1, 0.3), }), projectile_gravity: None, }, @@ -303,9 +313,7 @@ impl Tool { col: (1.0, 0.75, 0.11).into(), ..Default::default() }), - projectile_particles: Some(ParticleEmitter { - mode: ParticleEmitterMode::Sprinkler, - }), + projectile_particles: Some(ParticleEmitter::default()), projectile_gravity: None, }, ], diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index 215ce17959..39f3f07054 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; +use std::time::Duration; use vek::*; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -54,16 +55,35 @@ pub enum ParticleEmitterMode { #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ParticleEmitter { pub mode: ParticleEmitterMode, + + // spawn X particles per Y, that live for Z + // pub model_ref: &str, // can we have some kind of stack based key like a u8? + pub count: (u8, u8), + pub frequency: Duration, + + // relative to Pos, Ori components? + // can these be functions that returns a Vec3? + pub initial_lifespan: Duration, + pub initial_offset: (Vec3, Vec3), // fn() -> Vec3, + pub initial_scale: (f32, f32), // fn() -> Vec3, + pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, } impl Default for ParticleEmitter { fn default() -> Self { Self { mode: ParticleEmitterMode::Sprinkler, + // model_key: "voxygen.voxel.not_found", + count: (2, 5), + frequency: Duration::from_millis(500), + initial_lifespan: Duration::from_secs(2), + initial_offset: (vek::Vec3::broadcast(-5.0), vek::Vec3::broadcast(5.0)), + initial_orientation: (vek::Vec3::broadcast(-5.0), vek::Vec3::broadcast(5.0)), + initial_scale: (0.1, 2.0), } } } impl Component for ParticleEmitter { - type Storage = FlaggedStorage>; + type Storage = FlaggedStorage>; } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 090e587a40..c6942b4090 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -23,9 +23,9 @@ use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; use vek::*; use world::util::Sampler; +use comp::visual::ParticleEmitterMode; use scan_fmt::{scan_fmt, scan_fmt_some}; use tracing::error; -use comp::visual::ParticleEmitterMode; pub trait ChatCommandExt { fn execute(&self, server: &mut Server, entity: EcsEntity, args: String); @@ -911,9 +911,7 @@ fn handle_light( .create_entity_synced() .with(pos) .with(comp::ForceUpdate) - .with(comp::ParticleEmitter { - mode: ParticleEmitterMode::Sprinkler, - }) + .with(comp::ParticleEmitter::default()) .with(light_emitter); if let Some(light_offset) = light_offset_opt { builder.with(light_offset).build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index fd25760dce..83ff38a94c 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -118,9 +118,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { flicker: 1.0, animated: true, }) - .with(ParticleEmitter { - mode: ParticleEmitterMode::Sprinkler, - }) + .with(ParticleEmitter::default()) .with(WaypointArea::default()) .build(); } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index c0cd9cff1d..75b41cd8d0 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -16,44 +16,71 @@ use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::Rng; use specs::{Entity as EcsEntity, Join, WorldExt}; +use std::time::{Duration, Instant}; use tracing::{debug, error, warn}; use vek::{Mat4, Rgb, Vec3}; + struct Particles { // this is probably nieve, // could cache and re-use between particles, // should be a cache key? // model: Model, + // created_at: Instant, + // lifespan: Duration, + alive_until: Instant, // created_at + lifespan + instances: Instances, } -pub struct ParticleMgr { - entity_particles: HashMap>, - model_cache: Model, +struct Emitter { + last_emit: Instant, } +pub struct ParticleMgr { + // to keep track of spawn intervals + emitters: HashMap, + + // to keep track of lifespans + particles: Vec, + + model_cache: HashMap<&'static str, Model>, + + beginning_of_time: Instant, +} + +const MODEL_KEY: &str = "voxygen.voxel.not_found"; + impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { - let offset = Vec3::zero(); - let lod_scale = Vec3::one(); + let mut model_cache = HashMap::new(); - // TODO: from cache - let vox = assets::load_expect::("voxygen.voxel.not_found"); + let model = model_cache.entry(MODEL_KEY).or_insert_with(|| { + let offset = Vec3::zero(); + let lod_scale = Vec3::one(); - // TODO: from cache - let mesh = &Meshable::::generate_mesh( - &Segment::from(vox.as_ref()), - (offset * lod_scale, Vec3::one() / lod_scale), - ) - .0; + // TODO: from cache + let vox = assets::load_expect::(MODEL_KEY); - // TODO: from cache - let model = renderer - .create_model(mesh) - .expect("Failed to create particle model"); + // TODO: from cache + let mesh = &Meshable::::generate_mesh( + &Segment::from(vox.as_ref()), + (offset * lod_scale, Vec3::one() / lod_scale), + ) + .0; + + // TODO: from cache + let model = renderer + .create_model(mesh) + .expect("Failed to create particle model"); + + model + }); Self { - entity_particles: HashMap::new(), - model_cache: model, + emitters: HashMap::new(), + particles: Vec::new(), + model_cache, + beginning_of_time: Instant::now(), } } @@ -71,10 +98,14 @@ impl ParticleMgr { let tick = scene_data.tick; - // remove dead particles + let now = Instant::now(); + let beginning_of_time1 = self.beginning_of_time.clone(); - // remove dead entities, with dead particles - self.entity_particles.retain(|k, v| ecs.is_alive(*k)); + // remove dead emitters + self.emitters.retain(|k, _v| ecs.is_alive(*k)); + + // remove dead particles + self.particles.retain(|p| p.alive_until > now); // add living entities particles for (_i, (entity, particle_emitter, pos, ori, vel)) in ( @@ -87,10 +118,25 @@ impl ParticleMgr { .join() .enumerate() { - let entry = self - .entity_particles - .entry(entity) - .or_insert_with(|| into_particles(renderer, tick, particle_emitter, pos, ori, vel)); + let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { + last_emit: beginning_of_time1, // self.beginning_of_time.clone() + }); + + if emitter.last_emit + particle_emitter.frequency < now { + emitter.last_emit = Instant::now(); + + let cpu_insts = + into_particle_instances(particle_emitter, renderer, tick, pos, ori, vel); + + let gpu_insts = renderer + .create_instances(&cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + let entry = self.particles.push(Particles { + alive_until: now + particle_emitter.initial_lifespan, + instances: gpu_insts, + }); + } } } @@ -102,56 +148,54 @@ impl ParticleMgr { shadows: &Consts, focus_pos: Vec3, ) { - for particles in self.entity_particles.values() { - for particle in particles { - renderer.render_particles( - &self.model_cache, - globals, - &particle.instances, - lights, - shadows, - ); - } + for particle in &self.particles { + renderer.render_particles( + &self + .model_cache + .get(MODEL_KEY) + .expect("Expected particle model in cache"), + globals, + &particle.instances, + lights, + shadows, + ); } } } -fn into_particles( +fn into_particle_instances( + particle_emitter: &ParticleEmitter, renderer: &mut Renderer, tick: u64, - particle_emitter: &ParticleEmitter, pos: &Pos, ori: Option<&Ori>, vel: Option<&Vel>, -) -> Vec { +) -> Vec { let mut rng = rand::thread_rng(); - - let desired_instance_count = 100; - - // let ori_default = Ori::default(); let vel_default = Vel::default(); - - // let ori2 = ori.unwrap_or_else(|| &ori_default); let vel2 = vel.unwrap_or_else(|| &vel_default).0; + let mut instances_vec = Vec::new(); - for x in 0..desired_instance_count { + for x in 0..rng.gen_range(particle_emitter.count.0, particle_emitter.count.1) { // how does ParticleEmitterMode fit in here? // can we have a ParticleInstance type per ParticleEmitterMode? // can we mix and match instance types in the same instances_vec? instances_vec.push(ParticleInstance::new( Mat4::identity() // initial rotation - .rotated_x(rng.gen_range(0.0, 3.14 * 2.0)) - .rotated_y(rng.gen_range(0.0, 3.14 * 2.0)) - .rotated_z(rng.gen_range(0.0, 3.14 * 2.0)) + .rotated_x(rng.gen_range(particle_emitter.initial_orientation.0.x * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.x * std::f32::consts::PI * 2.0)) + .rotated_y(rng.gen_range(particle_emitter.initial_orientation.0.y * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.y * std::f32::consts::PI * 2.0)) + .rotated_z(rng.gen_range(particle_emitter.initial_orientation.0.z * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.z * std::f32::consts::PI * 2.0)) + // initial scale + .scaled_3d(rng.gen_range(particle_emitter.initial_scale.0, particle_emitter.initial_scale.1)) // inition position .translated_3d( - pos.0 + pos.0 // relative + Vec3::new( - rng.gen_range(-5.0, 5.0), - rng.gen_range(-5.0, 5.0), - rng.gen_range(0.0, 10.0), + rng.gen_range(particle_emitter.initial_offset.0.x, particle_emitter.initial_offset.1.x), + rng.gen_range(particle_emitter.initial_offset.0.y, particle_emitter.initial_offset.1.y), + rng.gen_range(particle_emitter.initial_offset.0.z, particle_emitter.initial_offset.1.z), ), ), Rgb::broadcast(1.0), // instance color @@ -162,9 +206,5 @@ fn into_particles( )); } - let instances = renderer - .create_instances(&instances_vec) - .expect("Failed to upload particle instances to the GPU!"); - - vec![Particles { instances }] + instances_vec } From 803677f0fb877ca5aea513881b376f5468f65d5d Mon Sep 17 00:00:00 2001 From: scott-c Date: Mon, 13 Jul 2020 21:44:28 +0800 Subject: [PATCH 35/71] Add particle velocity and ability particle emitter --- assets/voxygen/shaders/particle-vert.glsl | 21 +--- assets/voxygen/voxel/particle.vox | 3 + common/src/comp/ability.rs | 4 +- common/src/comp/inventory/item/tool.rs | 13 +-- common/src/comp/mod.rs | 2 +- common/src/comp/visual.rs | 20 +++- common/src/event.rs | 2 +- common/src/msg/ecs_packet.rs | 6 +- common/src/state.rs | 2 +- common/src/states/basic_ranged.rs | 10 +- server/src/cmd.rs | 3 +- server/src/events/entity_creation.rs | 11 +- server/src/sys/sentinel.rs | 14 +-- voxygen/src/render/pipelines/particle.rs | 8 +- voxygen/src/scene/particle.rs | 125 ++++++++++++++++++---- 15 files changed, 160 insertions(+), 84 deletions(-) create mode 100644 assets/voxygen/voxel/particle.vox diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index a50cc9a6a9..fcd4616023 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -12,7 +12,7 @@ in vec4 inst_mat2; in vec4 inst_mat3; in vec3 inst_col; in vec3 inst_vel; -in vec4 inst_tick; +in vec4 inst_time; in float inst_wind_sway; out vec3 f_pos; @@ -30,20 +30,10 @@ void main() { inst_mat[2] = inst_mat2; inst_mat[3] = inst_mat3; - vec3 particle_pos = (inst_mat * vec4(0, 0, 0, 1)).xyz; - f_pos = (inst_mat * vec4(v_pos * SCALE, 1)).xyz; - f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); + //f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); - // Wind waving - //f_pos += inst_wind_sway * vec3( - // sin(tick.x * 1.5 + f_pos.y * 0.1) * sin(tick.x * 0.35), - // sin(tick.x * 1.5 + f_pos.x * 0.1) * sin(tick.x * 0.25), - // 0.0 - //) * pow(abs(v_pos.z) * SCALE, 1.3) * 0.2; - - float elapsed = (tick.x - inst_tick.x) / 100000.0; - f_pos += (inst_vel * elapsed); + f_pos += inst_vel * (tick.x - inst_time.x); // First 3 normals are negative, next 3 are positive vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); @@ -53,11 +43,6 @@ void main() { f_col = srgb_to_linear(col) * srgb_to_linear(inst_col); f_ao = float((v_norm_ao >> 3) & 0x3u) / 4.0; - // Select glowing - if (select_pos.w > 0 && select_pos.xyz == floor(particle_pos)) { - f_col *= 4.0; - } - f_light = 1.0; gl_Position = diff --git a/assets/voxygen/voxel/particle.vox b/assets/voxygen/voxel/particle.vox new file mode 100644 index 0000000000..1f8fde6d26 --- /dev/null +++ b/assets/voxygen/voxel/particle.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba07287771bbfa8f369d0d634a6f847138b605962362517af16bec1e2a7c7951 +size 64 diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index e37e6bf26d..d656e8d4e6 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -62,7 +62,7 @@ pub enum CharacterAbility { projectile: Projectile, projectile_body: Body, projectile_light: Option, - projectile_particles: Option, + projectile_particles: Vec, projectile_gravity: Option, }, Boost { @@ -261,7 +261,7 @@ impl From<&CharacterAbility> for CharacterState { projectile: projectile.clone(), projectile_body: *projectile_body, projectile_light: *projectile_light, - projectile_particles: *projectile_particles, + projectile_particles: projectile_particles.clone().to_vec(), projectile_gravity: *projectile_gravity, }), CharacterAbility::Boost { duration, only_up } => CharacterState::Boost(boost::Data { diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index fc594bd7df..e86dc1a34b 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -180,7 +180,7 @@ impl Tool { }, projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_particles: None, + projectile_particles: vec![], projectile_gravity: Some(Gravity(0.2)), }, ChargedRanged { @@ -195,7 +195,7 @@ impl Tool { recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_particles: None, + projectile_particles: vec![], projectile_gravity: Some(Gravity(0.05)), }, ], @@ -276,7 +276,7 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), - projectile_particles: Some(ParticleEmitter { + projectile_particles: vec![ParticleEmitter { mode: ParticleEmitterMode::Sprinkler, // model_key: "voxygen.voxel.not_found", count: (2, 3), @@ -288,7 +288,8 @@ impl Tool { vek::Vec3::broadcast(1.0), ), initial_scale: (0.1, 0.3), - }), + initial_velocity: (vek::Vec3::zero(), vek::Vec3::one()), + }], projectile_gravity: None, }, BasicRanged { @@ -313,7 +314,7 @@ impl Tool { col: (1.0, 0.75, 0.11).into(), ..Default::default() }), - projectile_particles: Some(ParticleEmitter::default()), + projectile_particles: vec![ParticleEmitter::default()], projectile_gravity: None, }, ], @@ -358,7 +359,7 @@ impl Tool { col: (0.0, 1.0, 0.33).into(), ..Default::default() }), - projectile_particles: None, + projectile_particles: vec![], projectile_gravity: None, }, ], diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 4126c1a9df..3326cf2288 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -53,4 +53,4 @@ pub use player::{Player, MAX_MOUNT_RANGE_SQR}; pub use projectile::Projectile; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; pub use stats::{Exp, HealthChange, HealthSource, Level, Stats}; -pub use visual::{LightAnimation, LightEmitter, ParticleEmitter}; +pub use visual::{LightAnimation, LightEmitter, ParticleEmitter, ParticleEmitters}; diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index 39f3f07054..fd3904dc0a 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -52,6 +52,13 @@ pub enum ParticleEmitterMode { Sprinkler, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // Copy +pub struct ParticleEmitters(pub Vec); + +impl Default for ParticleEmitters { + fn default() -> Self { Self(vec![ParticleEmitter::default()]) } +} + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ParticleEmitter { pub mode: ParticleEmitterMode, @@ -67,6 +74,7 @@ pub struct ParticleEmitter { pub initial_offset: (Vec3, Vec3), // fn() -> Vec3, pub initial_scale: (f32, f32), // fn() -> Vec3, pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, + pub initial_velocity: (Vec3, Vec3), // fn() -> Vec3, } impl Default for ParticleEmitter { @@ -75,11 +83,15 @@ impl Default for ParticleEmitter { mode: ParticleEmitterMode::Sprinkler, // model_key: "voxygen.voxel.not_found", count: (2, 5), - frequency: Duration::from_millis(500), - initial_lifespan: Duration::from_secs(2), - initial_offset: (vek::Vec3::broadcast(-5.0), vek::Vec3::broadcast(5.0)), - initial_orientation: (vek::Vec3::broadcast(-5.0), vek::Vec3::broadcast(5.0)), + frequency: Duration::from_millis(100), + initial_lifespan: Duration::from_secs(20), + initial_offset: (vek::Vec3::broadcast(-0.1), vek::Vec3::broadcast(0.1)), + initial_orientation: (vek::Vec3::broadcast(0.0), vek::Vec3::broadcast(1.0)), initial_scale: (0.1, 2.0), + initial_velocity: ( + vek::Vec3::new(0.0, 0.0, 0.2), + vek::Vec3::new(0.01, 0.01, 1.0), + ), } } } diff --git a/common/src/event.rs b/common/src/event.rs index f291971c25..36e595154e 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -43,7 +43,7 @@ pub enum ServerEvent { dir: Dir, body: comp::Body, light: Option, - particles: Option, + particles: Vec, projectile: comp::Projectile, gravity: Option, }, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index e8fa12c33d..2b7512e234 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -15,7 +15,7 @@ sum_type! { Stats(comp::Stats), Energy(comp::Energy), LightEmitter(comp::LightEmitter), - ParticleEmitter(comp::ParticleEmitter), + ParticleEmitter(comp::ParticleEmitters), Item(comp::Item), Scale(comp::Scale), Group(comp::Group), @@ -43,7 +43,7 @@ sum_type! { Stats(PhantomData), Energy(PhantomData), LightEmitter(PhantomData), - ParticleEmitter(PhantomData), + ParticleEmitter(PhantomData), Item(PhantomData), Scale(PhantomData), Group(PhantomData), @@ -126,7 +126,7 @@ impl sync::CompPacket for EcsCompPacket { sync::handle_remove::(entity, world) }, EcsCompPhantom::ParticleEmitter(_) => { - sync::handle_remove::(entity, world) + sync::handle_remove::(entity, world) }, EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), diff --git a/common/src/state.rs b/common/src/state.rs index c4798c5813..a06ee25dce 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -113,7 +113,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs index 647b166230..8e5acaa967 100644 --- a/common/src/states/basic_ranged.rs +++ b/common/src/states/basic_ranged.rs @@ -20,7 +20,7 @@ pub struct Data { pub projectile: Projectile, pub projectile_body: Body, pub projectile_light: Option, - pub projectile_particles: Option, + pub projectile_particles: Vec, pub projectile_gravity: Option, /// Whether the attack fired already pub exhausted: bool, @@ -49,7 +49,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles, + projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: false, }); @@ -63,7 +63,7 @@ impl CharacterBehavior for Data { body: self.projectile_body, projectile, light: self.projectile_light, - particles: self.projectile_particles, + particles: self.projectile_particles.clone(), gravity: self.projectile_gravity, }); @@ -75,7 +75,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles, + projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: true, }); @@ -92,7 +92,7 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles, + projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: true, }); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index c6942b4090..c96265e33c 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -23,7 +23,6 @@ use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; use vek::*; use world::util::Sampler; -use comp::visual::ParticleEmitterMode; use scan_fmt::{scan_fmt, scan_fmt_some}; use tracing::error; @@ -911,7 +910,7 @@ fn handle_light( .create_entity_synced() .with(pos) .with(comp::ForceUpdate) - .with(comp::ParticleEmitter::default()) + .with(comp::ParticleEmitters::default()) .with(light_emitter); if let Some(light_offset) = light_offset_opt { builder.with(light_offset).build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 83ff38a94c..d6725fc87f 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -2,11 +2,10 @@ use crate::{sys, Server, StateExt}; use common::{ comp::{ self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, - ParticleEmitter, Pos, Projectile, Scale, Stats, Vel, WaypointArea, + ParticleEmitter, ParticleEmitters, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, util::Dir, }; -use comp::visual::ParticleEmitterMode; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -78,7 +77,7 @@ pub fn handle_shoot( dir: Dir, body: Body, light: Option, - particles: Option, + particles: Vec, projectile: Projectile, gravity: Option, ) { @@ -98,9 +97,7 @@ pub fn handle_shoot( if let Some(light) = light { builder = builder.with(light) } - if let Some(particles) = particles { - builder = builder.with(particles) - } + builder = builder.with(ParticleEmitters(particles)); if let Some(gravity) = gravity { builder = builder.with(gravity) } @@ -118,7 +115,7 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { flicker: 1.0, animated: true, }) - .with(ParticleEmitter::default()) + .with(ParticleEmitters::default()) .with(WaypointArea::default()) .build(); } diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 0fe4af0a3d..97e86c3480 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,8 +2,8 @@ use super::SysTimer; use common::{ comp::{ Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, - Loadout, Mass, MountState, Mounting, Ori, ParticleEmitter, Player, Pos, Scale, Stats, - Sticky, Vel, + Loadout, Mass, MountState, Mounting, Ori, ParticleEmitter, ParticleEmitters, Player, Pos, + Scale, Stats, Sticky, Vel, }, msg::EcsCompPacket, sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt}, @@ -45,7 +45,7 @@ pub struct TrackedComps<'a> { pub energy: ReadStorage<'a, Energy>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, - pub particle_emitter: ReadStorage<'a, ParticleEmitter>, + pub particle_emitter: ReadStorage<'a, ParticleEmitters>, pub item: ReadStorage<'a, Item>, pub scale: ReadStorage<'a, Scale>, pub mounting: ReadStorage<'a, Mounting>, @@ -96,7 +96,7 @@ impl<'a> TrackedComps<'a> { .map(|c| comps.push(c.into())); self.particle_emitter .get(entity) - .copied() + .cloned() .map(|c| comps.push(c.into())); self.item.get(entity).cloned().map(|c| comps.push(c.into())); self.scale @@ -153,7 +153,7 @@ pub struct ReadTrackers<'a> { pub energy: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, - pub particle_emitter: ReadExpect<'a, UpdateTracker>, + pub particle_emitter: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, pub scale: ReadExpect<'a, UpdateTracker>, pub mounting: ReadExpect<'a, UpdateTracker>, @@ -223,7 +223,7 @@ pub struct WriteTrackers<'a> { energy: WriteExpect<'a, UpdateTracker>, can_build: WriteExpect<'a, UpdateTracker>, light_emitter: WriteExpect<'a, UpdateTracker>, - particle_emitter: WriteExpect<'a, UpdateTracker>, + particle_emitter: WriteExpect<'a, UpdateTracker>, item: WriteExpect<'a, UpdateTracker>, scale: WriteExpect<'a, UpdateTracker>, mounting: WriteExpect<'a, UpdateTracker>, @@ -304,7 +304,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); - world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 1352549054..37c15a4577 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -28,7 +28,7 @@ gfx_defines! { inst_mat3: [f32; 4] = "inst_mat3", inst_col: [f32; 3] = "inst_col", inst_vel: [f32; 3] = "inst_vel", - inst_tick: [f32; 4] = "inst_tick", + inst_time: [f32; 4] = "inst_time", inst_wind_sway: f32 = "inst_wind_sway", mode: u8 = "mode", } @@ -74,7 +74,7 @@ impl Instance { mat: Mat4, col: Rgb, vel: Vec3, - tick: u64, + time: f64, wind_sway: f32, mode: ParticleEmitterMode, ) -> Self { @@ -86,7 +86,7 @@ impl Instance { inst_mat3: mat_arr[3], inst_col: col.into_array(), inst_vel: vel.into_array(), - inst_tick: [tick as f32; 4], + inst_time: [time as f32; 4], inst_wind_sway: wind_sway, @@ -101,7 +101,7 @@ impl Default for Instance { Mat4::identity(), Rgb::broadcast(1.0), Vec3::zero(), - 0, + 0.0, 0.0, ParticleEmitterMode::Sprinkler, ) diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 75b41cd8d0..3ed56be91a 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -2,22 +2,23 @@ use super::SceneData; use crate::{ mesh::Meshable, render::{ - mesh::Quad, Consts, Globals, Instances, Light, Mesh, Model, ParticleInstance, - ParticlePipeline, Renderer, Shadow, + mesh::Quad, Consts, Globals, Instances, Light, Model, ParticleInstance, ParticlePipeline, + Renderer, Shadow, }, }; use common::{ assets, - comp::{visual::ParticleEmitterMode, Ori, ParticleEmitter, Pos, Vel}, + comp::{ + visual::ParticleEmitterMode, CharacterState, Ori, ParticleEmitter, ParticleEmitters, Pos, + Vel, + }, figure::Segment, - vol::BaseVol, }; use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::Rng; use specs::{Entity as EcsEntity, Join, WorldExt}; use std::time::{Duration, Instant}; -use tracing::{debug, error, warn}; use vek::{Mat4, Rgb, Vec3}; struct Particles { @@ -48,7 +49,7 @@ pub struct ParticleMgr { beginning_of_time: Instant, } -const MODEL_KEY: &str = "voxygen.voxel.not_found"; +const MODEL_KEY: &str = "voxygen.voxel.particle"; impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { @@ -93,24 +94,34 @@ impl ParticleMgr { view_mat: Mat4, proj_mat: Mat4, ) { + let now = Instant::now(); let state = scene_data.state; let ecs = state.ecs(); - let tick = scene_data.tick; - - let now = Instant::now(); - let beginning_of_time1 = self.beginning_of_time.clone(); - // remove dead emitters self.emitters.retain(|k, _v| ecs.is_alive(*k)); // remove dead particles self.particles.retain(|p| p.alive_until > now); - // add living entities particles - for (_i, (entity, particle_emitter, pos, ori, vel)) in ( + // add ParticleEmitter particles + self.maintain_particle_emitter(renderer, scene_data); + + self.maintain_ability_particles(renderer, scene_data); + } + + fn maintain_particle_emitter(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + let state = scene_data.state; + let ecs = state.ecs(); + + let time = state.get_time(); + + let now = Instant::now(); + let beginning_of_time1 = self.beginning_of_time.clone(); + + for (_i, (entity, particle_emitters, pos, ori, vel)) in ( &ecs.entities(), - &ecs.read_storage::(), + &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), @@ -118,15 +129,78 @@ impl ParticleMgr { .join() .enumerate() { - let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { - last_emit: beginning_of_time1, // self.beginning_of_time.clone() - }); + for particle_emitter in &particle_emitters.0 { + // TODO: track multiple particle_emitter last_emit + let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { + last_emit: beginning_of_time1, // self.beginning_of_time.clone() + }); - if emitter.last_emit + particle_emitter.frequency < now { - emitter.last_emit = Instant::now(); + if emitter.last_emit + particle_emitter.frequency < now { + emitter.last_emit = Instant::now(); + + let cpu_insts = + into_particle_instances(&particle_emitter, renderer, time, pos, ori, vel); + + let gpu_insts = renderer + .create_instances(&cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + let entry = self.particles.push(Particles { + alive_until: now + particle_emitter.initial_lifespan, + instances: gpu_insts, + }); + } + } + } + } + + fn maintain_ability_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + let state = scene_data.state; + let ecs = state.ecs(); + + let time = state.get_time(); + + let now = Instant::now(); + let beginning_of_time1 = self.beginning_of_time.clone(); + + for (_i, (entity, pos, character_state)) in ( + &ecs.entities(), + //&ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + // let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { + // last_emit: beginning_of_time1, // self.beginning_of_time.clone() + // }); + + // if emitter.last_emit + particle_emitter.frequency < now { + // emitter.last_emit = Instant::now(); + // } + + if let CharacterState::BasicMelee(melee_data) = character_state { + // TODO: configure the emitter on the ability instead. + let particle_emitter = ParticleEmitter { + count: (30, 50), + frequency: Duration::from_millis(1000), // doesn't matter + initial_lifespan: Duration::from_millis(1000), + initial_offset: ( + Vec3::new(1.0, -1.0, 0.0), + Vec3::new(1.01, 1.0, 2.0), /* TODO: cone // melee_data.max_angle */ + ), + initial_orientation: (Vec3::zero(), Vec3::one()), + initial_scale: (1.0, 3.0), + mode: ParticleEmitterMode::Sprinkler, + initial_velocity: ( + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(10.0, 0.01, 0.01), /* TODO: cone // melee_data.max_angle */ + ), + }; let cpu_insts = - into_particle_instances(particle_emitter, renderer, tick, pos, ori, vel); + into_particle_instances(&particle_emitter, renderer, time, pos, None, None); let gpu_insts = renderer .create_instances(&cpu_insts) @@ -166,7 +240,7 @@ impl ParticleMgr { fn into_particle_instances( particle_emitter: &ParticleEmitter, renderer: &mut Renderer, - tick: u64, + time: f64, pos: &Pos, ori: Option<&Ori>, vel: Option<&Vel>, @@ -199,8 +273,13 @@ fn into_particle_instances( ), ), Rgb::broadcast(1.0), // instance color - vel2 + Vec3::broadcast(rng.gen_range(-5.0, 5.0)), - tick, + vel2 // relative + + Vec3::new( + rng.gen_range(particle_emitter.initial_velocity.0.x, particle_emitter.initial_velocity.1.x), + rng.gen_range(particle_emitter.initial_velocity.0.y, particle_emitter.initial_velocity.1.y), + rng.gen_range(particle_emitter.initial_velocity.0.z, particle_emitter.initial_velocity.1.z), + ), + time, rng.gen_range(0.0, 20.0), // wind sway ParticleEmitterMode::Sprinkler, // particle_emitter.mode */ )); From 3139e85dff04818e4c785c9d9652518c0eab3e7e Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 14 Jul 2020 23:08:05 +0800 Subject: [PATCH 36/71] allow for col particles --- common/src/comp/inventory/item/tool.rs | 1 + common/src/comp/visual.rs | 26 +++++++++++++++++++------- voxygen/src/scene/particle.rs | 7 ++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index e86dc1a34b..86746daf2f 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -289,6 +289,7 @@ impl Tool { ), initial_scale: (0.1, 0.3), initial_velocity: (vek::Vec3::zero(), vek::Vec3::one()), + initial_col: (vek::Rgb::zero(), vek::Rgb::one()), }], projectile_gravity: None, }, diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index fd3904dc0a..0159c2945e 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -56,7 +56,20 @@ pub enum ParticleEmitterMode { pub struct ParticleEmitters(pub Vec); impl Default for ParticleEmitters { - fn default() -> Self { Self(vec![ParticleEmitter::default()]) } + fn default() -> Self { + Self(vec![ParticleEmitter::default(), ParticleEmitter { + mode: ParticleEmitterMode::Sprinkler, + // model_key: "voxygen.voxel.not_found", + count: (7, 10), + frequency: Duration::from_millis(100), + initial_lifespan: Duration::from_secs(500), + initial_offset: (Vec3::broadcast(-0.2), Vec3::broadcast(0.2)), + initial_orientation: (Vec3::broadcast(0.0), Vec3::broadcast(1.0)), + initial_scale: (1.0, 2.0), + initial_velocity: (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.01, 0.01, 3.0)), + initial_col: (Rgb::new(0.999, 0.0, 0.0), Rgb::new(1.0, 1.0, 0.001)), + }]) + } } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -75,6 +88,7 @@ pub struct ParticleEmitter { pub initial_scale: (f32, f32), // fn() -> Vec3, pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, pub initial_velocity: (Vec3, Vec3), // fn() -> Vec3, + pub initial_col: (Rgb, Rgb), // fn() -> Vec3, } impl Default for ParticleEmitter { @@ -85,13 +99,11 @@ impl Default for ParticleEmitter { count: (2, 5), frequency: Duration::from_millis(100), initial_lifespan: Duration::from_secs(20), - initial_offset: (vek::Vec3::broadcast(-0.1), vek::Vec3::broadcast(0.1)), - initial_orientation: (vek::Vec3::broadcast(0.0), vek::Vec3::broadcast(1.0)), + initial_offset: (Vec3::broadcast(-0.1), Vec3::broadcast(0.1)), + initial_orientation: (Vec3::broadcast(0.0), Vec3::broadcast(1.0)), initial_scale: (0.1, 2.0), - initial_velocity: ( - vek::Vec3::new(0.0, 0.0, 0.2), - vek::Vec3::new(0.01, 0.01, 1.0), - ), + initial_velocity: (Vec3::new(0.0, 0.0, 0.2), Vec3::new(0.01, 0.01, 1.0)), + initial_col: (Rgb::new(0.999, 0.999, 0.999), Rgb::new(1.0, 1.0, 1.0)), } } } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 3ed56be91a..96af3493a7 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -197,6 +197,7 @@ impl ParticleMgr { Vec3::new(1.0, 0.0, 0.0), Vec3::new(10.0, 0.01, 0.01), /* TODO: cone // melee_data.max_angle */ ), + initial_col: (Rgb::zero(), Rgb::one()), }; let cpu_insts = @@ -272,7 +273,11 @@ fn into_particle_instances( rng.gen_range(particle_emitter.initial_offset.0.z, particle_emitter.initial_offset.1.z), ), ), - Rgb::broadcast(1.0), // instance color + Rgb::new( + rng.gen_range(particle_emitter.initial_col.0.r, particle_emitter.initial_col.1.r), + rng.gen_range(particle_emitter.initial_col.0.g, particle_emitter.initial_col.1.g), + rng.gen_range(particle_emitter.initial_col.0.b, particle_emitter.initial_col.1.b), + ), // instance color vel2 // relative + Vec3::new( rng.gen_range(particle_emitter.initial_velocity.0.x, particle_emitter.initial_velocity.1.x), From 4bc373a832e438573fc4d1e873ed41ae1e278ad5 Mon Sep 17 00:00:00 2001 From: scott-c Date: Wed, 15 Jul 2020 23:45:47 +0800 Subject: [PATCH 37/71] remove particle emitter component --- assets/voxygen/shaders/particle-vert.glsl | 53 +++++- common/src/comp/ability.rs | 6 +- common/src/comp/inventory/item/tool.rs | 23 +-- common/src/comp/mod.rs | 2 +- common/src/comp/phys.rs | 4 + common/src/comp/visual.rs | 115 ++++++------ common/src/event.rs | 1 - common/src/msg/ecs_packet.rs | 7 - common/src/state.rs | 1 - common/src/states/basic_ranged.rs | 7 +- common/src/util/dir.rs | 2 + server/src/cmd.rs | 1 - server/src/events/entity_creation.rs | 7 +- server/src/events/mod.rs | 3 +- server/src/sys/sentinel.rs | 19 +- voxygen/src/render/pipelines/particle.rs | 71 +++---- voxygen/src/scene/mod.rs | 18 +- voxygen/src/scene/particle.rs | 219 ++++++---------------- voxygen/src/session.rs | 12 +- 19 files changed, 220 insertions(+), 351 deletions(-) diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index fcd4616023..c51431b5d5 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -10,10 +10,9 @@ in vec4 inst_mat0; in vec4 inst_mat1; in vec4 inst_mat2; in vec4 inst_mat3; -in vec3 inst_col; -in vec3 inst_vel; -in vec4 inst_time; -in float inst_wind_sway; +in float inst_time; +in float inst_entropy; +in int inst_mode; out vec3 f_pos; flat out vec3 f_norm; @@ -23,6 +22,17 @@ out float f_light; const float SCALE = 1.0 / 11.0; +float PHI = 1.61803398874989484820459; // Φ = Golden Ratio + +float gold_noise(in vec2 xy, in float seed){ + return fract(tan(distance(xy * PHI, xy) * seed) * xy.x); +} + +// Modes +const int SMOKE = 0; +const int FIRE = 1; +const int FLAMETHROWER = 2; + void main() { mat4 inst_mat; inst_mat[0] = inst_mat0; @@ -30,10 +40,39 @@ void main() { inst_mat[2] = inst_mat2; inst_mat[3] = inst_mat3; - f_pos = (inst_mat * vec4(v_pos * SCALE, 1)).xyz; - //f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); + float rand1 = gold_noise(vec2(0.0, 0.0), inst_entropy); + float rand2 = gold_noise(vec2(1.0, 1.0), inst_entropy); + float rand3 = gold_noise(vec2(2.0, 2.0), inst_entropy); + float rand4 = gold_noise(vec2(3.0, 3.0), inst_entropy); + float rand5 = gold_noise(vec2(4.0, 4.0), inst_entropy); + float rand6 = gold_noise(vec2(5.0, 5.0), inst_entropy); - f_pos += inst_vel * (tick.x - inst_time.x); + vec3 inst_vel = vec3(0.0, 0.0, 0.0); + vec3 inst_pos = vec3(0.0, 0.0, 0.0); + vec3 inst_col = vec3(1.0, 1.0, 1.0); + + if (inst_mode == SMOKE) { + inst_col = vec3(1.0, 1.0, 1.0); + inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 1.0 + rand3); + inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + } else if (inst_mode == FIRE) { + inst_col = vec3(1.0, 1.0 * inst_entropy, 0.0); + inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 4.0 + rand3); + inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + } else if (inst_mode == FLAMETHROWER) { + // TODO: velocity based on attack range, angle and parent orientation. + inst_col = vec3(1.0, 1.0 * inst_entropy, 0.0); + inst_vel = vec3(rand1 * 0.1, rand2 * 0.1, 3.0 + rand3); + inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + } else { + inst_col = vec3(rand1, rand2, rand3); + inst_vel = vec3(rand4, rand5, rand6); + inst_pos = vec3(rand1, rand2, rand3); + } + + f_pos = (inst_mat * vec4((v_pos + inst_pos) * SCALE, 1)).xyz; + + f_pos += inst_vel * (tick.x - inst_time); // First 3 normals are negative, next 3 are positive vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index d656e8d4e6..79292b797e 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -2,8 +2,7 @@ use crate::{ comp::{ ability::Stage, item::{armor::Protection, Item, ItemKind}, - Body, CharacterState, EnergySource, Gravity, LightEmitter, ParticleEmitter, Projectile, - StateUpdate, + Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile, StateUpdate, }, states::{triple_strike::*, *}, sys::character_behavior::JoinData, @@ -62,7 +61,6 @@ pub enum CharacterAbility { projectile: Projectile, projectile_body: Body, projectile_light: Option, - projectile_particles: Vec, projectile_gravity: Option, }, Boost { @@ -249,7 +247,6 @@ impl From<&CharacterAbility> for CharacterState { projectile, projectile_body, projectile_light, - projectile_particles, projectile_gravity, energy_cost: _, } => CharacterState::BasicRanged(basic_ranged::Data { @@ -261,7 +258,6 @@ impl From<&CharacterAbility> for CharacterState { projectile: projectile.clone(), projectile_body: *projectile_body, projectile_light: *projectile_light, - projectile_particles: projectile_particles.clone().to_vec(), projectile_gravity: *projectile_gravity, }), CharacterAbility::Boost { duration, only_up } => CharacterState::Boost(boost::Data { diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 86746daf2f..8b09cc7324 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -2,8 +2,8 @@ // version in voxygen\src\meta.rs in order to reset save files to being empty use crate::comp::{ - body::object, projectile, visual::ParticleEmitterMode, Body, CharacterAbility, Gravity, - HealthChange, HealthSource, LightEmitter, ParticleEmitter, Projectile, + body::object, projectile, Body, CharacterAbility, Gravity, HealthChange, HealthSource, + LightEmitter, Projectile, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -180,7 +180,6 @@ impl Tool { }, projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_particles: vec![], projectile_gravity: Some(Gravity(0.2)), }, ChargedRanged { @@ -195,7 +194,6 @@ impl Tool { recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_particles: vec![], projectile_gravity: Some(Gravity(0.05)), }, ], @@ -276,21 +274,6 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), - projectile_particles: vec![ParticleEmitter { - mode: ParticleEmitterMode::Sprinkler, - // model_key: "voxygen.voxel.not_found", - count: (2, 3), - frequency: Duration::from_millis(50), - initial_lifespan: Duration::from_millis(500), - initial_offset: (vek::Vec3::broadcast(-1.0), vek::Vec3::broadcast(1.0)), - initial_orientation: ( - vek::Vec3::broadcast(-1.0), - vek::Vec3::broadcast(1.0), - ), - initial_scale: (0.1, 0.3), - initial_velocity: (vek::Vec3::zero(), vek::Vec3::one()), - initial_col: (vek::Rgb::zero(), vek::Rgb::one()), - }], projectile_gravity: None, }, BasicRanged { @@ -315,7 +298,6 @@ impl Tool { col: (1.0, 0.75, 0.11).into(), ..Default::default() }), - projectile_particles: vec![ParticleEmitter::default()], projectile_gravity: None, }, ], @@ -360,7 +342,6 @@ impl Tool { col: (0.0, 1.0, 0.33).into(), ..Default::default() }), - projectile_particles: vec![], projectile_gravity: None, }, ], diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 3326cf2288..963f3c95a6 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -53,4 +53,4 @@ pub use player::{Player, MAX_MOUNT_RANGE_SQR}; pub use projectile::Projectile; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; pub use stats::{Exp, HealthChange, HealthSource, Level, Stats}; -pub use visual::{LightAnimation, LightEmitter, ParticleEmitter, ParticleEmitters}; +pub use visual::{LightAnimation, LightEmitter}; diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index e9496f047c..b6fdb18d73 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -24,6 +24,10 @@ impl Component for Vel { #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct Ori(pub Dir); +impl Ori { + pub fn vec(&self) -> &Vec3 { &self.0.vec() } +} + impl Component for Ori { type Storage = IdvStorage; } diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index 0159c2945e..b8f463d593 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; -use std::time::Duration; use vek::*; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -47,67 +46,67 @@ impl Default for LightAnimation { impl Component for LightAnimation { type Storage = FlaggedStorage>; } -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum ParticleEmitterMode { - Sprinkler, -} +// #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +// pub enum ParticleEmitterMode { +// Sprinkler, +// } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // Copy -pub struct ParticleEmitters(pub Vec); +// #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // Copy +// pub struct ParticleEmitters(pub Vec); -impl Default for ParticleEmitters { - fn default() -> Self { - Self(vec![ParticleEmitter::default(), ParticleEmitter { - mode: ParticleEmitterMode::Sprinkler, - // model_key: "voxygen.voxel.not_found", - count: (7, 10), - frequency: Duration::from_millis(100), - initial_lifespan: Duration::from_secs(500), - initial_offset: (Vec3::broadcast(-0.2), Vec3::broadcast(0.2)), - initial_orientation: (Vec3::broadcast(0.0), Vec3::broadcast(1.0)), - initial_scale: (1.0, 2.0), - initial_velocity: (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.01, 0.01, 3.0)), - initial_col: (Rgb::new(0.999, 0.0, 0.0), Rgb::new(1.0, 1.0, 0.001)), - }]) - } -} +// impl Default for ParticleEmitters { +// fn default() -> Self { +// Self(vec![ParticleEmitter::default(), ParticleEmitter { +// mode: ParticleEmitterMode::Sprinkler, +// // model_key: "voxygen.voxel.not_found", +// count: (7, 10), +// frequency: Duration::from_millis(100), +// initial_lifespan: Duration::from_millis(500), +// initial_offset: (Vec3::broadcast(-0.2), Vec3::broadcast(0.2)), +// initial_orientation: (Vec3::broadcast(0.0), +// Vec3::broadcast(1.0)), initial_scale: (1.0, 2.5), +// initial_velocity: (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.01, +// 0.01, 3.0)), initial_col: (Rgb::new(0.999, 0.0, 0.0), +// Rgb::new(1.0, 1.0, 0.001)), }]) +// } +// } -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ParticleEmitter { - pub mode: ParticleEmitterMode, +// #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +// pub struct ParticleEmitter { +// pub mode: ParticleEmitterMode, - // spawn X particles per Y, that live for Z - // pub model_ref: &str, // can we have some kind of stack based key like a u8? - pub count: (u8, u8), - pub frequency: Duration, +// // spawn X particles per Y, that live for Z +// // pub model_ref: &str, // can we have some kind of stack based key like +// a u8? pub count: (u8, u8), +// pub frequency: Duration, - // relative to Pos, Ori components? - // can these be functions that returns a Vec3? - pub initial_lifespan: Duration, - pub initial_offset: (Vec3, Vec3), // fn() -> Vec3, - pub initial_scale: (f32, f32), // fn() -> Vec3, - pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, - pub initial_velocity: (Vec3, Vec3), // fn() -> Vec3, - pub initial_col: (Rgb, Rgb), // fn() -> Vec3, -} +// // relative to Pos, Ori components? +// // can these be functions that returns a Vec3? +// pub initial_lifespan: Duration, +// pub initial_offset: (Vec3, Vec3), // fn() -> Vec3, +// pub initial_scale: (f32, f32), // fn() -> Vec3, +// pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, +// pub initial_velocity: (Vec3, Vec3), // fn() -> Vec3, +// pub initial_col: (Rgb, Rgb), // fn() -> Vec3, +// } -impl Default for ParticleEmitter { - fn default() -> Self { - Self { - mode: ParticleEmitterMode::Sprinkler, - // model_key: "voxygen.voxel.not_found", - count: (2, 5), - frequency: Duration::from_millis(100), - initial_lifespan: Duration::from_secs(20), - initial_offset: (Vec3::broadcast(-0.1), Vec3::broadcast(0.1)), - initial_orientation: (Vec3::broadcast(0.0), Vec3::broadcast(1.0)), - initial_scale: (0.1, 2.0), - initial_velocity: (Vec3::new(0.0, 0.0, 0.2), Vec3::new(0.01, 0.01, 1.0)), - initial_col: (Rgb::new(0.999, 0.999, 0.999), Rgb::new(1.0, 1.0, 1.0)), - } - } -} +// impl Default for ParticleEmitter { +// fn default() -> Self { +// Self { +// mode: ParticleEmitterMode::Sprinkler, +// // model_key: "voxygen.voxel.not_found", +// count: (2, 5), +// frequency: Duration::from_millis(100), +// initial_lifespan: Duration::from_secs(20), +// initial_offset: (Vec3::broadcast(-0.1), Vec3::broadcast(0.1)), +// initial_orientation: (Vec3::broadcast(0.0), +// Vec3::broadcast(1.0)), initial_scale: (0.1, 2.0), +// initial_velocity: (Vec3::new(0.0, 0.0, 0.2), Vec3::new(0.01, +// 0.01, 1.0)), initial_col: (Rgb::new(0.999, 0.999, 0.999), +// Rgb::new(1.0, 1.0, 1.0)), } +// } +// } -impl Component for ParticleEmitter { - type Storage = FlaggedStorage>; -} +// impl Component for ParticleEmitter { +// type Storage = FlaggedStorage>; +// } diff --git a/common/src/event.rs b/common/src/event.rs index 36e595154e..943757a2a7 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -43,7 +43,6 @@ pub enum ServerEvent { dir: Dir, body: comp::Body, light: Option, - particles: Vec, projectile: comp::Projectile, gravity: Option, }, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index 2b7512e234..726d67321a 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -15,7 +15,6 @@ sum_type! { Stats(comp::Stats), Energy(comp::Energy), LightEmitter(comp::LightEmitter), - ParticleEmitter(comp::ParticleEmitters), Item(comp::Item), Scale(comp::Scale), Group(comp::Group), @@ -43,7 +42,6 @@ sum_type! { Stats(PhantomData), Energy(PhantomData), LightEmitter(PhantomData), - ParticleEmitter(PhantomData), Item(PhantomData), Scale(PhantomData), Group(PhantomData), @@ -71,7 +69,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(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::ParticleEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world), @@ -97,7 +94,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(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::ParticleEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world), @@ -125,9 +121,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::(entity, world) }, - EcsCompPhantom::ParticleEmitter(_) => { - sync::handle_remove::(entity, world) - }, EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Group(_) => sync::handle_remove::(entity, world), diff --git a/common/src/state.rs b/common/src/state.rs index a06ee25dce..3d003b7a4f 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -113,7 +113,6 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs index 8e5acaa967..b41bd6c208 100644 --- a/common/src/states/basic_ranged.rs +++ b/common/src/states/basic_ranged.rs @@ -1,5 +1,5 @@ use crate::{ - comp::{Body, CharacterState, Gravity, LightEmitter, ParticleEmitter, Projectile, StateUpdate}, + comp::{Body, CharacterState, Gravity, LightEmitter, Projectile, StateUpdate}, event::ServerEvent, states::utils::*, sys::character_behavior::*, @@ -20,7 +20,6 @@ pub struct Data { pub projectile: Projectile, pub projectile_body: Body, pub projectile_light: Option, - pub projectile_particles: Vec, pub projectile_gravity: Option, /// Whether the attack fired already pub exhausted: bool, @@ -49,7 +48,6 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: false, }); @@ -63,7 +61,6 @@ impl CharacterBehavior for Data { body: self.projectile_body, projectile, light: self.projectile_light, - particles: self.projectile_particles.clone(), gravity: self.projectile_gravity, }); @@ -75,7 +72,6 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: true, }); @@ -92,7 +88,6 @@ impl CharacterBehavior for Data { projectile: self.projectile.clone(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, - projectile_particles: self.projectile_particles.clone(), projectile_gravity: self.projectile_gravity, exhausted: true, }); diff --git a/common/src/util/dir.rs b/common/src/util/dir.rs index 22aa1b0fe6..96c085479d 100644 --- a/common/src/util/dir.rs +++ b/common/src/util/dir.rs @@ -87,6 +87,8 @@ impl Dir { } pub fn is_valid(&self) -> bool { !self.0.map(f32::is_nan).reduce_or() && self.is_normalized() } + + pub fn vec(&self) -> &Vec3 { &self.0 } } impl std::ops::Deref for Dir { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index c96265e33c..b580cfbd98 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -910,7 +910,6 @@ fn handle_light( .create_entity_synced() .with(pos) .with(comp::ForceUpdate) - .with(comp::ParticleEmitters::default()) .with(light_emitter); if let Some(light_offset) = light_offset_opt { builder.with(light_offset).build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index d6725fc87f..a62c0b342c 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -1,8 +1,8 @@ use crate::{sys, Server, StateExt}; use common::{ comp::{ - self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, - ParticleEmitter, ParticleEmitters, Pos, Projectile, Scale, Stats, Vel, WaypointArea, + self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, + Projectile, Scale, Stats, Vel, WaypointArea, }, util::Dir, }; @@ -77,7 +77,6 @@ pub fn handle_shoot( dir: Dir, body: Body, light: Option, - particles: Vec, projectile: Projectile, gravity: Option, ) { @@ -97,7 +96,6 @@ pub fn handle_shoot( if let Some(light) = light { builder = builder.with(light) } - builder = builder.with(ParticleEmitters(particles)); if let Some(gravity) = gravity { builder = builder.with(gravity) } @@ -115,7 +113,6 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { flicker: 1.0, animated: true, }) - .with(ParticleEmitters::default()) .with(WaypointArea::default()) .build(); } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 9761e1d869..412d9a536a 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -61,10 +61,9 @@ impl Server { dir, body, light, - particles, projectile, gravity, - } => handle_shoot(self, entity, dir, body, light, particles, projectile, gravity), + } => handle_shoot(self, entity, dir, body, light, projectile, gravity), ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change), ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 97e86c3480..6121dbb4a9 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,7 +2,7 @@ use super::SysTimer; use common::{ comp::{ Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, - Loadout, Mass, MountState, Mounting, Ori, ParticleEmitter, ParticleEmitters, Player, Pos, + Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, }, msg::EcsCompPacket, @@ -45,7 +45,6 @@ pub struct TrackedComps<'a> { pub energy: ReadStorage<'a, Energy>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, - pub particle_emitter: ReadStorage<'a, ParticleEmitters>, pub item: ReadStorage<'a, Item>, pub scale: ReadStorage<'a, Scale>, pub mounting: ReadStorage<'a, Mounting>, @@ -94,10 +93,6 @@ impl<'a> TrackedComps<'a> { .get(entity) .copied() .map(|c| comps.push(c.into())); - self.particle_emitter - .get(entity) - .cloned() - .map(|c| comps.push(c.into())); self.item.get(entity).cloned().map(|c| comps.push(c.into())); self.scale .get(entity) @@ -153,7 +148,6 @@ pub struct ReadTrackers<'a> { pub energy: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, - pub particle_emitter: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, pub scale: ReadExpect<'a, UpdateTracker>, pub mounting: ReadExpect<'a, UpdateTracker>, @@ -187,12 +181,6 @@ impl<'a> ReadTrackers<'a> { &comps.light_emitter, filter, ) - .with_component( - &comps.uid, - &*self.particle_emitter, - &comps.particle_emitter, - filter, - ) .with_component(&comps.uid, &*self.item, &comps.item, filter) .with_component(&comps.uid, &*self.scale, &comps.scale, filter) .with_component(&comps.uid, &*self.mounting, &comps.mounting, filter) @@ -223,7 +211,6 @@ pub struct WriteTrackers<'a> { energy: WriteExpect<'a, UpdateTracker>, can_build: WriteExpect<'a, UpdateTracker>, light_emitter: WriteExpect<'a, UpdateTracker>, - particle_emitter: WriteExpect<'a, UpdateTracker>, item: WriteExpect<'a, UpdateTracker>, scale: WriteExpect<'a, UpdateTracker>, mounting: WriteExpect<'a, UpdateTracker>, @@ -246,9 +233,6 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.energy.record_changes(&comps.energy); trackers.can_build.record_changes(&comps.can_build); trackers.light_emitter.record_changes(&comps.light_emitter); - trackers - .particle_emitter - .record_changes(&comps.particle_emitter); trackers.item.record_changes(&comps.item); trackers.scale.record_changes(&comps.scale); trackers.mounting.record_changes(&comps.mounting); @@ -304,7 +288,6 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); - world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 37c15a4577..9b4a94606f 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -2,7 +2,6 @@ use super::{ super::{Pipeline, TgtColorFmt, TgtDepthStencilFmt}, Globals, Light, Shadow, }; -use common::comp::visual::ParticleEmitterMode; use gfx::{ self, gfx_defines, gfx_impl_struct_meta, gfx_pipeline, gfx_pipeline_inner, gfx_vertex_struct_meta, @@ -22,15 +21,26 @@ gfx_defines! { } vertex Instance { + // created_at time, so we can calculate time relativity, needed for relative animation. + // can save 32 bits per instance, for particles that are not relatively animated. + inst_time: f32 = "inst_time", + + // a seed value for randomness + inst_entropy: f32 = "inst_entropy", + + // modes should probably be seperate shaders, as a part of scaling and optimisation efforts + inst_mode: i32 = "inst_mode", + + // a triangle is: f32 x 3 x 3 x 1 = 288 bits + // a quad is: f32 x 3 x 3 x 2 = 576 bits + // a cube is: f32 x 3 x 3 x 12 = 3456 bits + // this matrix is: f32 x 4 x 4 x 1 = 512 bits (per instance!) + // consider using vertex postion & entropy instead; + // to determine initial offset, scale, orientation etc. inst_mat0: [f32; 4] = "inst_mat0", inst_mat1: [f32; 4] = "inst_mat1", inst_mat2: [f32; 4] = "inst_mat2", inst_mat3: [f32; 4] = "inst_mat3", - inst_col: [f32; 3] = "inst_col", - inst_vel: [f32; 3] = "inst_vel", - inst_time: [f32; 4] = "inst_time", - inst_wind_sway: f32 = "inst_wind_sway", - mode: u8 = "mode", } pipeline pipe { @@ -69,43 +79,38 @@ impl Vertex { } } +pub enum ParticleMode { + CampfireSmoke, + CampfireFire, +} + +impl ParticleMode { + pub fn into_uint(self) -> u32 { self as u32 } +} + impl Instance { pub fn new( - mat: Mat4, - col: Rgb, - vel: Vec3, - time: f64, - wind_sway: f32, - mode: ParticleEmitterMode, + inst_time: f64, + inst_entropy: f32, + inst_mode: ParticleMode, + inst_mat: Mat4, ) -> Self { - let mat_arr = mat.into_col_arrays(); + let inst_mat_col = inst_mat.into_col_arrays(); Self { - inst_mat0: mat_arr[0], - inst_mat1: mat_arr[1], - inst_mat2: mat_arr[2], - inst_mat3: mat_arr[3], - inst_col: col.into_array(), - inst_vel: vel.into_array(), - inst_time: [time as f32; 4], + inst_time: inst_time as f32, + inst_entropy, + inst_mode: inst_mode as i32, - inst_wind_sway: wind_sway, - - mode: mode as u8, + inst_mat0: inst_mat_col[0], + inst_mat1: inst_mat_col[1], + inst_mat2: inst_mat_col[2], + inst_mat3: inst_mat_col[3], } } } impl Default for Instance { - fn default() -> Self { - Self::new( - Mat4::identity(), - Rgb::broadcast(1.0), - Vec3::zero(), - 0.0, - 0.0, - ParticleEmitterMode::Sprinkler, - ) - } + fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Mat4::identity()) } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 9b0f6c3fe5..8e7186cfb6 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -398,14 +398,7 @@ impl Scene { self.figure_mgr.clean(scene_data.tick); // Maintain the particles. - self.particle_mgr.maintain( - renderer, - &scene_data, - self.camera.get_focus_pos(), - self.loaded_distance, - view_mat, - proj_mat, - ); + self.particle_mgr.maintain(renderer, &scene_data); // Maintain audio self.sfx_mgr @@ -442,13 +435,8 @@ impl Scene { scene_data.figure_lod_render_distance, ); - self.particle_mgr.render( - renderer, - &self.globals, - &self.lights, - &self.shadows, - self.camera.get_focus_pos(), - ); + self.particle_mgr + .render(renderer, &self.globals, &self.lights, &self.shadows); // Render the skybox. renderer.render_skybox(&self.skybox.model, &self.globals, &self.skybox.locals); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 96af3493a7..3477f52d87 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -2,51 +2,31 @@ use super::SceneData; use crate::{ mesh::Meshable, render::{ - mesh::Quad, Consts, Globals, Instances, Light, Model, ParticleInstance, ParticlePipeline, - Renderer, Shadow, + pipelines::particle::ParticleMode, Consts, Globals, Instances, Light, Model, + ParticleInstance, ParticlePipeline, Renderer, Shadow, }, }; use common::{ assets, - comp::{ - visual::ParticleEmitterMode, CharacterState, Ori, ParticleEmitter, ParticleEmitters, Pos, - Vel, - }, + comp::{object, Body, CharacterState, Pos}, figure::Segment, }; use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::Rng; -use specs::{Entity as EcsEntity, Join, WorldExt}; +use specs::{Join, WorldExt}; use std::time::{Duration, Instant}; -use vek::{Mat4, Rgb, Vec3}; +use vek::{Mat4, Vec3}; struct Particles { - // this is probably nieve, - // could cache and re-use between particles, - // should be a cache key? - // model: Model, - // created_at: Instant, - // lifespan: Duration, alive_until: Instant, // created_at + lifespan - instances: Instances, } -struct Emitter { - last_emit: Instant, -} - pub struct ParticleMgr { - // to keep track of spawn intervals - emitters: HashMap, - - // to keep track of lifespans + // keep track of lifespans particles: Vec, - model_cache: HashMap<&'static str, Model>, - - beginning_of_time: Instant, } const MODEL_KEY: &str = "voxygen.voxel.particle"; @@ -55,21 +35,18 @@ impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { let mut model_cache = HashMap::new(); - let model = model_cache.entry(MODEL_KEY).or_insert_with(|| { + model_cache.entry(MODEL_KEY).or_insert_with(|| { let offset = Vec3::zero(); let lod_scale = Vec3::one(); - // TODO: from cache let vox = assets::load_expect::(MODEL_KEY); - // TODO: from cache let mesh = &Meshable::::generate_mesh( &Segment::from(vox.as_ref()), (offset * lod_scale, Vec3::one() / lod_scale), ) .0; - // TODO: from cache let model = renderer .create_model(mesh) .expect("Failed to create particle model"); @@ -78,137 +55,103 @@ impl ParticleMgr { }); Self { - emitters: HashMap::new(), particles: Vec::new(), model_cache, - beginning_of_time: Instant::now(), } } - pub fn maintain( - &mut self, - renderer: &mut Renderer, - scene_data: &SceneData, - focus_pos: Vec3, - loaded_distance: f32, - view_mat: Mat4, - proj_mat: Mat4, - ) { + pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { let now = Instant::now(); - let state = scene_data.state; - let ecs = state.ecs(); - - // remove dead emitters - self.emitters.retain(|k, _v| ecs.is_alive(*k)); // remove dead particles self.particles.retain(|p| p.alive_until > now); - // add ParticleEmitter particles - self.maintain_particle_emitter(renderer, scene_data); + self.maintain_waypoint_particles(renderer, scene_data); - self.maintain_ability_particles(renderer, scene_data); + self.maintain_boost_particles(renderer, scene_data); } - fn maintain_particle_emitter(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + fn maintain_waypoint_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); - let time = state.get_time(); - let now = Instant::now(); - let beginning_of_time1 = self.beginning_of_time.clone(); + let mut rng = rand::thread_rng(); - for (_i, (entity, particle_emitters, pos, ori, vel)) in ( + for (_i, (_entity, pos, body)) in ( &ecs.entities(), - &ecs.read_storage::(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), + &ecs.read_storage::(), ) .join() .enumerate() { - for particle_emitter in &particle_emitters.0 { - // TODO: track multiple particle_emitter last_emit - let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { - last_emit: beginning_of_time1, // self.beginning_of_time.clone() - }); + match body { + Body::Object(object::Body::CampfireLit) => { + let fire_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireFire, + Mat4::identity().translated_3d(pos.0), + )]; - if emitter.last_emit + particle_emitter.frequency < now { - emitter.last_emit = Instant::now(); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instances: renderer + .create_instances(&fire_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"), + }); - let cpu_insts = - into_particle_instances(&particle_emitter, renderer, time, pos, ori, vel); + let smoke_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + Mat4::identity().translated_3d(pos.0), + )]; - let gpu_insts = renderer - .create_instances(&cpu_insts) + let smoke_cpu_insts = renderer + .create_instances(&smoke_cpu_insts) .expect("Failed to upload particle instances to the GPU!"); - let entry = self.particles.push(Particles { - alive_until: now + particle_emitter.initial_lifespan, - instances: gpu_insts, + self.particles.push(Particles { + alive_until: now + Duration::from_secs(10), + instances: smoke_cpu_insts, }); - } + }, + _ => {}, } } } - fn maintain_ability_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + fn maintain_boost_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); - let time = state.get_time(); - let now = Instant::now(); - let beginning_of_time1 = self.beginning_of_time.clone(); + let mut rng = rand::thread_rng(); - for (_i, (entity, pos, character_state)) in ( + for (_i, (_entity, pos, character_state)) in ( &ecs.entities(), - //&ecs.read_storage::(), &ecs.read_storage::(), &ecs.read_storage::(), ) .join() .enumerate() { - // let emitter = self.emitters.entry(entity).or_insert_with(|| Emitter { - // last_emit: beginning_of_time1, // self.beginning_of_time.clone() - // }); - - // if emitter.last_emit + particle_emitter.frequency < now { - // emitter.last_emit = Instant::now(); - // } - - if let CharacterState::BasicMelee(melee_data) = character_state { - // TODO: configure the emitter on the ability instead. - let particle_emitter = ParticleEmitter { - count: (30, 50), - frequency: Duration::from_millis(1000), // doesn't matter - initial_lifespan: Duration::from_millis(1000), - initial_offset: ( - Vec3::new(1.0, -1.0, 0.0), - Vec3::new(1.01, 1.0, 2.0), /* TODO: cone // melee_data.max_angle */ - ), - initial_orientation: (Vec3::zero(), Vec3::one()), - initial_scale: (1.0, 3.0), - mode: ParticleEmitterMode::Sprinkler, - initial_velocity: ( - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(10.0, 0.01, 0.01), /* TODO: cone // melee_data.max_angle */ - ), - initial_col: (Rgb::zero(), Rgb::one()), - }; - - let cpu_insts = - into_particle_instances(&particle_emitter, renderer, time, pos, None, None); + if let CharacterState::Boost(_) = character_state { + let cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + Mat4::identity().translated_3d(pos.0), + )]; let gpu_insts = renderer .create_instances(&cpu_insts) .expect("Failed to upload particle instances to the GPU!"); - let entry = self.particles.push(Particles { - alive_until: now + particle_emitter.initial_lifespan, + self.particles.push(Particles { + alive_until: now + Duration::from_secs(15), instances: gpu_insts, }); } @@ -221,7 +164,6 @@ impl ParticleMgr { globals: &Consts, lights: &Consts, shadows: &Consts, - focus_pos: Vec3, ) { for particle in &self.particles { renderer.render_particles( @@ -237,58 +179,3 @@ impl ParticleMgr { } } } - -fn into_particle_instances( - particle_emitter: &ParticleEmitter, - renderer: &mut Renderer, - time: f64, - pos: &Pos, - ori: Option<&Ori>, - vel: Option<&Vel>, -) -> Vec { - let mut rng = rand::thread_rng(); - let vel_default = Vel::default(); - let vel2 = vel.unwrap_or_else(|| &vel_default).0; - - let mut instances_vec = Vec::new(); - - for x in 0..rng.gen_range(particle_emitter.count.0, particle_emitter.count.1) { - // how does ParticleEmitterMode fit in here? - // can we have a ParticleInstance type per ParticleEmitterMode? - // can we mix and match instance types in the same instances_vec? - instances_vec.push(ParticleInstance::new( - Mat4::identity() - // initial rotation - .rotated_x(rng.gen_range(particle_emitter.initial_orientation.0.x * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.x * std::f32::consts::PI * 2.0)) - .rotated_y(rng.gen_range(particle_emitter.initial_orientation.0.y * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.y * std::f32::consts::PI * 2.0)) - .rotated_z(rng.gen_range(particle_emitter.initial_orientation.0.z * std::f32::consts::PI * 2.0, particle_emitter.initial_orientation.1.z * std::f32::consts::PI * 2.0)) - // initial scale - .scaled_3d(rng.gen_range(particle_emitter.initial_scale.0, particle_emitter.initial_scale.1)) - // inition position - .translated_3d( - pos.0 // relative - + Vec3::new( - rng.gen_range(particle_emitter.initial_offset.0.x, particle_emitter.initial_offset.1.x), - rng.gen_range(particle_emitter.initial_offset.0.y, particle_emitter.initial_offset.1.y), - rng.gen_range(particle_emitter.initial_offset.0.z, particle_emitter.initial_offset.1.z), - ), - ), - Rgb::new( - rng.gen_range(particle_emitter.initial_col.0.r, particle_emitter.initial_col.1.r), - rng.gen_range(particle_emitter.initial_col.0.g, particle_emitter.initial_col.1.g), - rng.gen_range(particle_emitter.initial_col.0.b, particle_emitter.initial_col.1.b), - ), // instance color - vel2 // relative - + Vec3::new( - rng.gen_range(particle_emitter.initial_velocity.0.x, particle_emitter.initial_velocity.1.x), - rng.gen_range(particle_emitter.initial_velocity.0.y, particle_emitter.initial_velocity.1.y), - rng.gen_range(particle_emitter.initial_velocity.0.z, particle_emitter.initial_velocity.1.z), - ), - time, - rng.gen_range(0.0, 20.0), // wind sway - ParticleEmitterMode::Sprinkler, // particle_emitter.mode */ - )); - } - - instances_vec -} diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index fb44b952a4..37d3a7e637 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -787,7 +787,7 @@ impl PlayState for SessionState { }, HudEvent::AdjustParticleRenderDistance(particle_render_distance) => { global_state.settings.graphics.particle_render_distance = - particle_render_distance; + particle_render_distance; global_state.settings.save_to_file_warn(); }, HudEvent::AdjustFigureLoDRenderDistance(figure_lod_render_distance) => { @@ -1002,9 +1002,12 @@ impl PlayState for SessionState { gamma: global_state.settings.graphics.gamma, mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable, sprite_render_distance: global_state.settings.graphics.sprite_render_distance - as f32, - particle_render_distance: global_state.settings.graphics.particle_render_distance - as f32, + as f32, + particle_render_distance: global_state + .settings + .graphics + .particle_render_distance + as f32, figure_lod_render_distance: global_state .settings .graphics @@ -1060,6 +1063,7 @@ impl PlayState for SessionState { mouse_smoothing: settings.gameplay.smooth_pan_enable, sprite_render_distance: settings.graphics.sprite_render_distance as f32, figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32, + particle_render_distance: settings.graphics.particle_render_distance as f32, is_aiming: self.is_aiming, }; self.scene.render( From bb2a5c885b37dda4a93195d3b44e45ae2fb3f832 Mon Sep 17 00:00:00 2001 From: scott-c Date: Sun, 19 Jul 2020 15:16:06 +0800 Subject: [PATCH 38/71] Add fireball and bomb particle effects --- assets/voxygen/shaders/particle-vert.glsl | 58 ++--- common/src/comp/visual.rs | 64 ------ voxygen/src/render/pipelines/particle.rs | 37 ++- voxygen/src/scene/particle.rs | 266 +++++++++++++++++----- 4 files changed, 265 insertions(+), 160 deletions(-) diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index c51431b5d5..54fac86749 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -6,10 +6,7 @@ in vec3 v_pos; in uint v_col; in uint v_norm_ao; -in vec4 inst_mat0; -in vec4 inst_mat1; -in vec4 inst_mat2; -in vec4 inst_mat3; +in vec3 inst_pos; in float inst_time; in float inst_entropy; in int inst_mode; @@ -22,7 +19,8 @@ out float f_light; const float SCALE = 1.0 / 11.0; -float PHI = 1.61803398874989484820459; // Φ = Golden Ratio +// Φ = Golden Ratio +float PHI = 1.61803398874989484820459; float gold_noise(in vec2 xy, in float seed){ return fract(tan(distance(xy * PHI, xy) * seed) * xy.x); @@ -31,46 +29,54 @@ float gold_noise(in vec2 xy, in float seed){ // Modes const int SMOKE = 0; const int FIRE = 1; -const int FLAMETHROWER = 2; +const int GUN_POWDER_SPARK = 2; + +// meters per second +const float earth_gravity = 9.807; + +mat4 translate(vec3 vec){ + return mat4( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(vec.x, vec.y, vec.z, 1.0) + ); +} void main() { - mat4 inst_mat; - inst_mat[0] = inst_mat0; - inst_mat[1] = inst_mat1; - inst_mat[2] = inst_mat2; - inst_mat[3] = inst_mat3; + mat4 inst_mat = translate(inst_pos); float rand1 = gold_noise(vec2(0.0, 0.0), inst_entropy); - float rand2 = gold_noise(vec2(1.0, 1.0), inst_entropy); - float rand3 = gold_noise(vec2(2.0, 2.0), inst_entropy); - float rand4 = gold_noise(vec2(3.0, 3.0), inst_entropy); - float rand5 = gold_noise(vec2(4.0, 4.0), inst_entropy); - float rand6 = gold_noise(vec2(5.0, 5.0), inst_entropy); + float rand2 = gold_noise(vec2(10.0, 10.0), inst_entropy); + float rand3 = gold_noise(vec2(20.0, 20.0), inst_entropy); + float rand4 = gold_noise(vec2(30.0, 30.0), inst_entropy); + float rand5 = gold_noise(vec2(40.0, 40.0), inst_entropy); + float rand6 = gold_noise(vec2(50.0, 50.0), inst_entropy); vec3 inst_vel = vec3(0.0, 0.0, 0.0); - vec3 inst_pos = vec3(0.0, 0.0, 0.0); + vec3 inst_pos2 = vec3(0.0, 0.0, 0.0); vec3 inst_col = vec3(1.0, 1.0, 1.0); if (inst_mode == SMOKE) { inst_col = vec3(1.0, 1.0, 1.0); inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 1.0 + rand3); - inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + inst_pos2 = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); } else if (inst_mode == FIRE) { inst_col = vec3(1.0, 1.0 * inst_entropy, 0.0); inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 4.0 + rand3); - inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); - } else if (inst_mode == FLAMETHROWER) { - // TODO: velocity based on attack range, angle and parent orientation. - inst_col = vec3(1.0, 1.0 * inst_entropy, 0.0); - inst_vel = vec3(rand1 * 0.1, rand2 * 0.1, 3.0 + rand3); - inst_pos = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + inst_pos2 = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + } else if (inst_mode == GUN_POWDER_SPARK) { + inst_col = vec3(1.0, 1.0, 0.0); + inst_vel = vec3(rand2 * 2.0 - 1.0, rand1 * 2.0 - 1.0, 5.0 + rand3); + inst_vel -= vec3(0.0, 0.0, earth_gravity * (tick.x - inst_time)); + inst_pos2 = vec3(0.0, 0.0, 0.0); } else { inst_col = vec3(rand1, rand2, rand3); inst_vel = vec3(rand4, rand5, rand6); - inst_pos = vec3(rand1, rand2, rand3); + inst_pos2 = vec3(rand1, rand2, rand3); } - f_pos = (inst_mat * vec4((v_pos + inst_pos) * SCALE, 1)).xyz; + f_pos = (inst_mat * vec4((v_pos + inst_pos2) * SCALE, 1)).xyz; f_pos += inst_vel * (tick.x - inst_time); diff --git a/common/src/comp/visual.rs b/common/src/comp/visual.rs index b8f463d593..013f27aa2c 100644 --- a/common/src/comp/visual.rs +++ b/common/src/comp/visual.rs @@ -46,67 +46,3 @@ impl Default for LightAnimation { impl Component for LightAnimation { type Storage = FlaggedStorage>; } -// #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -// pub enum ParticleEmitterMode { -// Sprinkler, -// } - -// #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // Copy -// pub struct ParticleEmitters(pub Vec); - -// impl Default for ParticleEmitters { -// fn default() -> Self { -// Self(vec![ParticleEmitter::default(), ParticleEmitter { -// mode: ParticleEmitterMode::Sprinkler, -// // model_key: "voxygen.voxel.not_found", -// count: (7, 10), -// frequency: Duration::from_millis(100), -// initial_lifespan: Duration::from_millis(500), -// initial_offset: (Vec3::broadcast(-0.2), Vec3::broadcast(0.2)), -// initial_orientation: (Vec3::broadcast(0.0), -// Vec3::broadcast(1.0)), initial_scale: (1.0, 2.5), -// initial_velocity: (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.01, -// 0.01, 3.0)), initial_col: (Rgb::new(0.999, 0.0, 0.0), -// Rgb::new(1.0, 1.0, 0.001)), }]) -// } -// } - -// #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -// pub struct ParticleEmitter { -// pub mode: ParticleEmitterMode, - -// // spawn X particles per Y, that live for Z -// // pub model_ref: &str, // can we have some kind of stack based key like -// a u8? pub count: (u8, u8), -// pub frequency: Duration, - -// // relative to Pos, Ori components? -// // can these be functions that returns a Vec3? -// pub initial_lifespan: Duration, -// pub initial_offset: (Vec3, Vec3), // fn() -> Vec3, -// pub initial_scale: (f32, f32), // fn() -> Vec3, -// pub initial_orientation: (Vec3, Vec3), // fn() -> Vec3, -// pub initial_velocity: (Vec3, Vec3), // fn() -> Vec3, -// pub initial_col: (Rgb, Rgb), // fn() -> Vec3, -// } - -// impl Default for ParticleEmitter { -// fn default() -> Self { -// Self { -// mode: ParticleEmitterMode::Sprinkler, -// // model_key: "voxygen.voxel.not_found", -// count: (2, 5), -// frequency: Duration::from_millis(100), -// initial_lifespan: Duration::from_secs(20), -// initial_offset: (Vec3::broadcast(-0.1), Vec3::broadcast(0.1)), -// initial_orientation: (Vec3::broadcast(0.0), -// Vec3::broadcast(1.0)), initial_scale: (0.1, 2.0), -// initial_velocity: (Vec3::new(0.0, 0.0, 0.2), Vec3::new(0.01, -// 0.01, 1.0)), initial_col: (Rgb::new(0.999, 0.999, 0.999), -// Rgb::new(1.0, 1.0, 1.0)), } -// } -// } - -// impl Component for ParticleEmitter { -// type Storage = FlaggedStorage>; -// } diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 9b4a94606f..1f467c638e 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -26,21 +26,24 @@ gfx_defines! { inst_time: f32 = "inst_time", // a seed value for randomness + // can save 32 bits per instance, for particles that don't need randomness/uniqueness. inst_entropy: f32 = "inst_entropy", - // modes should probably be seperate shaders, as a part of scaling and optimisation efforts + // modes should probably be seperate shaders, as a part of scaling and optimisation efforts. + // can save 32 bits per instance, and have cleaner tailor made code. inst_mode: i32 = "inst_mode", - // a triangle is: f32 x 3 x 3 x 1 = 288 bits - // a quad is: f32 x 3 x 3 x 2 = 576 bits - // a cube is: f32 x 3 x 3 x 12 = 3456 bits - // this matrix is: f32 x 4 x 4 x 1 = 512 bits (per instance!) - // consider using vertex postion & entropy instead; - // to determine initial offset, scale, orientation etc. - inst_mat0: [f32; 4] = "inst_mat0", - inst_mat1: [f32; 4] = "inst_mat1", - inst_mat2: [f32; 4] = "inst_mat2", - inst_mat3: [f32; 4] = "inst_mat3", + // a triangle is: f32 x 3 x 3 x 1 = 288 bits + // a quad is: f32 x 3 x 3 x 2 = 576 bits + // a cube is: f32 x 3 x 3 x 12 = 3456 bits + // this vec is: f32 x 3 x 1 x 1 = 96 bits (per instance!) + // consider using a throw-away mesh and + // positioning the vertex verticies instead, + // if we have: + // - a triangle mesh, and 3 or more instances. + // - a quad mesh, and 6 or more instances. + // - a cube mesh, and 36 or more instances. + inst_pos: [f32; 3] = "inst_pos", } pipeline pipe { @@ -82,6 +85,7 @@ impl Vertex { pub enum ParticleMode { CampfireSmoke, CampfireFire, + GunPowderSpark, } impl ParticleMode { @@ -93,24 +97,19 @@ impl Instance { inst_time: f64, inst_entropy: f32, inst_mode: ParticleMode, - inst_mat: Mat4, + inst_pos: Vec3, ) -> Self { - let inst_mat_col = inst_mat.into_col_arrays(); Self { inst_time: inst_time as f32, inst_entropy, inst_mode: inst_mode as i32, - - inst_mat0: inst_mat_col[0], - inst_mat1: inst_mat_col[1], - inst_mat2: inst_mat_col[2], - inst_mat3: inst_mat_col[3], + inst_pos: inst_pos.into_array(), } } } impl Default for Instance { - fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Mat4::identity()) } + fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 3477f52d87..5a0b299a7b 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -16,7 +16,7 @@ use hashbrown::HashMap; use rand::Rng; use specs::{Join, WorldExt}; use std::time::{Duration, Instant}; -use vek::{Mat4, Vec3}; +use vek::Vec3; struct Particles { alive_until: Instant, // created_at + lifespan @@ -66,63 +66,231 @@ impl ParticleMgr { // remove dead particles self.particles.retain(|p| p.alive_until > now); - self.maintain_waypoint_particles(renderer, scene_data); + self.maintain_body_particles(renderer, scene_data); self.maintain_boost_particles(renderer, scene_data); } - fn maintain_waypoint_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { - let state = scene_data.state; - let ecs = state.ecs(); - let time = state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - - for (_i, (_entity, pos, body)) in ( + fn maintain_body_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + let ecs = scene_data.state.ecs(); + for (_i, (_entity, body, pos)) in ( &ecs.entities(), - &ecs.read_storage::(), &ecs.read_storage::(), + &ecs.read_storage::(), ) .join() .enumerate() { match body { Body::Object(object::Body::CampfireLit) => { - let fire_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireFire, - Mat4::identity().translated_3d(pos.0), - )]; - - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instances: renderer - .create_instances(&fire_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"), - }); - - let smoke_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - Mat4::identity().translated_3d(pos.0), - )]; - - let smoke_cpu_insts = renderer - .create_instances(&smoke_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - - self.particles.push(Particles { - alive_until: now + Duration::from_secs(10), - instances: smoke_cpu_insts, - }); + self.maintain_campfirelit_particles(renderer, scene_data, pos) }, + Body::Object(object::Body::BoltFire) => { + self.maintain_boltfire_particles(renderer, scene_data, pos) + }, + Body::Object(object::Body::BoltFireBig) => { + self.maintain_boltfirebig_particles(renderer, scene_data, pos) + }, + Body::Object(object::Body::Bomb) => { + self.maintain_bomb_particles(renderer, scene_data, pos) + }, + // Body::Object(object::Body::Pouch) => { + // self.maintain_pouch_particles(renderer, scene_data, pos) + // }, _ => {}, } } } + fn maintain_campfirelit_particles( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + pos: &Pos, + ) { + let time = scene_data.state.get_time(); + let now = Instant::now(); + let mut rng = rand::thread_rng(); + + let fire_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireFire, + pos.0, + )]; + + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instances: renderer + .create_instances(&fire_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"), + }); + + let smoke_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + pos.0, + )]; + + let smoke_cpu_insts = renderer + .create_instances(&smoke_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + self.particles.push(Particles { + alive_until: now + Duration::from_secs(10), + instances: smoke_cpu_insts, + }); + } + + fn maintain_boltfire_particles( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + pos: &Pos, + ) { + let time = scene_data.state.get_time(); + let now = Instant::now(); + let mut rng = rand::thread_rng(); + + let fire_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireFire, + pos.0, + )]; + + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instances: renderer + .create_instances(&fire_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"), + }); + + let smoke_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + pos.0, + )]; + + let smoke_cpu_insts = renderer + .create_instances(&smoke_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + self.particles.push(Particles { + alive_until: now + Duration::from_secs(1), + instances: smoke_cpu_insts, + }); + } + + fn maintain_boltfirebig_particles( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + pos: &Pos, + ) { + let time = scene_data.state.get_time(); + let now = Instant::now(); + let mut rng = rand::thread_rng(); + + let fire_cpu_insts = vec![ + ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), + ]; + + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instances: renderer + .create_instances(&fire_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"), + }); + + let smoke_cpu_insts = vec![ + ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), + ]; + + let smoke_cpu_insts = renderer + .create_instances(&smoke_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + self.particles.push(Particles { + alive_until: now + Duration::from_secs(2), + instances: smoke_cpu_insts, + }); + } + + fn maintain_bomb_particles( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + pos: &Pos, + ) { + let time = scene_data.state.get_time(); + let now = Instant::now(); + let mut rng = rand::thread_rng(); + + let fire_cpu_insts = vec![ + ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + ]; + + self.particles.push(Particles { + alive_until: now + Duration::from_millis(1500), + instances: renderer + .create_instances(&fire_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"), + }); + + let smoke_cpu_insts = vec![ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + pos.0, + )]; + + let smoke_cpu_insts = renderer + .create_instances(&smoke_cpu_insts) + .expect("Failed to upload particle instances to the GPU!"); + + self.particles.push(Particles { + alive_until: now + Duration::from_secs(2), + instances: smoke_cpu_insts, + }); + } + + // fn maintain_pouch_particles( + // &mut self, + // renderer: &mut Renderer, + // scene_data: &SceneData, + // pos: &Pos, + // ) { + // let time = scene_data.state.get_time(); + // let now = Instant::now(); + // let mut rng = rand::thread_rng(); + + // let smoke_cpu_insts = vec![ParticleInstance::new( + // time, + // rng.gen(), + // ParticleMode::CampfireSmoke, + // pos.0, + // )]; + + // let smoke_cpu_insts = renderer + // .create_instances(&smoke_cpu_insts) + // .expect("Failed to upload particle instances to the GPU!"); + + // self.particles.push(Particles { + // alive_until: now + Duration::from_secs(1), + // instances: smoke_cpu_insts, + // }); + // } + fn maintain_boost_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); @@ -143,7 +311,7 @@ impl ParticleMgr { time, rng.gen(), ParticleMode::CampfireSmoke, - Mat4::identity().translated_3d(pos.0), + pos.0, )]; let gpu_insts = renderer @@ -165,17 +333,13 @@ impl ParticleMgr { lights: &Consts, shadows: &Consts, ) { + let model = &self + .model_cache + .get(MODEL_KEY) + .expect("Expected particle model in cache"); + for particle in &self.particles { - renderer.render_particles( - &self - .model_cache - .get(MODEL_KEY) - .expect("Expected particle model in cache"), - globals, - &particle.instances, - lights, - shadows, - ); + renderer.render_particles(model, globals, &particle.instances, lights, shadows); } } } From f9f9e9e1908c4f8bbeadee705304c98646b3f694 Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 21 Jul 2020 23:48:20 +0800 Subject: [PATCH 39/71] Add particle count to debug info --- voxygen/src/hud/mod.rs | 16 +++- voxygen/src/scene/mod.rs | 8 +- voxygen/src/scene/particle.rs | 176 ++++++++++++++-------------------- voxygen/src/session.rs | 3 + 4 files changed, 98 insertions(+), 105 deletions(-) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 7f8242af23..de2ac1649c 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -179,6 +179,7 @@ widget_ids! { entity_count, num_chunks, num_figures, + num_particles, // Game Version version, @@ -247,6 +248,8 @@ pub struct DebugInfo { pub num_visible_chunks: u32, pub num_figures: u32, pub num_figures_visible: u32, + pub num_particles: u32, + pub num_particles_visible: u32, } pub struct HudInfo { @@ -1496,6 +1499,17 @@ impl Hud { .font_size(self.fonts.cyri.scale(14)) .set(self.ids.num_figures, ui_widgets); + // Number of particles + Text::new(&format!( + "Particles: {} ({} visible)", + debug_info.num_particles, debug_info.num_particles_visible, + )) + .color(TEXT_COLOR) + .down_from(self.ids.num_figures, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.num_particles, ui_widgets); + // Help Window if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { Text::new( @@ -1505,7 +1519,7 @@ impl Hud { .replace("{key}", help_key.to_string().as_str()), ) .color(TEXT_COLOR) - .down_from(self.ids.num_figures, 5.0) + .down_from(self.ids.num_particles, 5.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.help_info, ui_widgets); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 8e7186cfb6..d242239e94 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -435,8 +435,12 @@ impl Scene { scene_data.figure_lod_render_distance, ); - self.particle_mgr - .render(renderer, &self.globals, &self.lights, &self.shadows); + self.particle_mgr.render( + renderer, + &self.globals, + &self.lights, + &self.shadows, + ); // Render the skybox. renderer.render_skybox(&self.skybox.model, &self.globals, &self.skybox.locals); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 5a0b299a7b..92ef68607e 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -20,12 +20,13 @@ use vek::Vec3; struct Particles { alive_until: Instant, // created_at + lifespan - instances: Instances, + instance: ParticleInstance, } pub struct ParticleMgr { // keep track of lifespans particles: Vec, + instances: Instances, model_cache: HashMap<&'static str, Model>, } @@ -54,21 +55,46 @@ impl ParticleMgr { model }); + let insts = Vec::new(); + + let instances = renderer + .create_instances(&insts) + .expect("Failed to upload particle instances to the GPU!"); + Self { particles: Vec::new(), + instances, model_cache, } } + pub fn particle_count(&self) -> usize { self.instances.count() } + + pub fn particle_count_visible(&self) -> usize { self.instances.count() } + pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { let now = Instant::now(); // remove dead particles self.particles.retain(|p| p.alive_until > now); + // let zxc = scene_data.particle_render_distance; + self.maintain_body_particles(renderer, scene_data); self.maintain_boost_particles(renderer, scene_data); + + let all_cpu_instances = self + .particles + .iter() + .map(|p| p.instance) + .collect::>(); + + // TODO: upload just the ones that were created and added to queue, not all of + // them. + self.instances = renderer + .create_instances(&all_cpu_instances) + .expect("Failed to upload particle instances to the GPU!"); } fn maintain_body_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { @@ -112,34 +138,14 @@ impl ParticleMgr { let now = Instant::now(); let mut rng = rand::thread_rng(); - let fire_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireFire, - pos.0, - )]; - self.particles.push(Particles { alive_until: now + Duration::from_millis(250), - instances: renderer - .create_instances(&fire_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), }); - let smoke_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - pos.0, - )]; - - let smoke_cpu_insts = renderer - .create_instances(&smoke_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - self.particles.push(Particles { alive_until: now + Duration::from_secs(10), - instances: smoke_cpu_insts, + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), }); } @@ -153,34 +159,14 @@ impl ParticleMgr { let now = Instant::now(); let mut rng = rand::thread_rng(); - let fire_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireFire, - pos.0, - )]; - self.particles.push(Particles { alive_until: now + Duration::from_millis(250), - instances: renderer - .create_instances(&fire_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), }); - let smoke_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - pos.0, - )]; - - let smoke_cpu_insts = renderer - .create_instances(&smoke_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - self.particles.push(Particles { alive_until: now + Duration::from_secs(1), - instances: smoke_cpu_insts, + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), }); } @@ -194,31 +180,28 @@ impl ParticleMgr { let now = Instant::now(); let mut rng = rand::thread_rng(); - let fire_cpu_insts = vec![ - ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - ]; - + // fire self.particles.push(Particles { alive_until: now + Duration::from_millis(250), - instances: renderer - .create_instances(&fire_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), }); - let smoke_cpu_insts = vec![ - ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - ]; - - let smoke_cpu_insts = renderer - .create_instances(&smoke_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - + // smoke self.particles.push(Particles { alive_until: now + Duration::from_secs(2), - instances: smoke_cpu_insts, + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_secs(2), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_secs(2), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), }); } @@ -232,35 +215,32 @@ impl ParticleMgr { let now = Instant::now(); let mut rng = rand::thread_rng(); - let fire_cpu_insts = vec![ - ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - ]; - + // sparks self.particles.push(Particles { alive_until: now + Duration::from_millis(1500), - instances: renderer - .create_instances(&fire_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(1500), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(1500), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(1500), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), + }); + self.particles.push(Particles { + alive_until: now + Duration::from_millis(1500), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), }); - let smoke_cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - pos.0, - )]; - - let smoke_cpu_insts = renderer - .create_instances(&smoke_cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - + // smoke self.particles.push(Particles { alive_until: now + Duration::from_secs(2), - instances: smoke_cpu_insts, + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), }); } @@ -307,20 +287,14 @@ impl ParticleMgr { .enumerate() { if let CharacterState::Boost(_) = character_state { - let cpu_insts = vec![ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - pos.0, - )]; - - let gpu_insts = renderer - .create_instances(&cpu_insts) - .expect("Failed to upload particle instances to the GPU!"); - self.particles.push(Particles { alive_until: now + Duration::from_secs(15), - instances: gpu_insts, + instance: ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + pos.0, + ), }); } } @@ -338,8 +312,6 @@ impl ParticleMgr { .get(MODEL_KEY) .expect("Expected particle model in cache"); - for particle in &self.particles { - renderer.render_particles(model, globals, &particle.instances, lights, shadows); - } + renderer.render_particles(model, globals, &self.instances, lights, shadows); } } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 37d3a7e637..86ea35b27a 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -689,6 +689,9 @@ impl PlayState for SessionState { num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32, num_figures: self.scene.figure_mgr().figure_count() as u32, num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32, + num_particles: self.scene.particle_mgr().particle_count() as u32, + num_particles_visible: self.scene.particle_mgr().particle_count_visible() + as u32, }, &self.scene.camera(), global_state.clock.get_last_delta(), From 5acfe44cbb266dc7b860beb407e10bdef3403e42 Mon Sep 17 00:00:00 2001 From: scott-c Date: Sat, 25 Jul 2020 23:46:45 +0800 Subject: [PATCH 40/71] Add toggle particles graphics setting --- assets/voxygen/i18n/en.ron | 1 + assets/voxygen/shaders/include/globals.glsl | 1 - assets/voxygen/shaders/particle-frag.glsl | 2 +- voxygen/src/hud/mod.rs | 5 +- voxygen/src/hud/settings_window.rs | 31 +++++- voxygen/src/render/pipelines/mod.rs | 6 +- voxygen/src/scene/mod.rs | 4 +- voxygen/src/scene/particle.rs | 115 ++++++-------------- voxygen/src/scene/simple.rs | 1 - voxygen/src/session.rs | 17 +-- voxygen/src/settings.rs | 4 +- 11 files changed, 79 insertions(+), 108 deletions(-) diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index e57424b780..b3a2004539 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -297,6 +297,7 @@ magically infused items?"#, "hud.settings.fluid_rendering_mode.cheap": "Cheap", "hud.settings.fluid_rendering_mode.shiny": "Shiny", "hud.settings.cloud_rendering_mode.regular": "Regular", + "hud.settings.particles": "Particles", "hud.settings.fullscreen": "Fullscreen", "hud.settings.save_window_size": "Save window size", diff --git a/assets/voxygen/shaders/include/globals.glsl b/assets/voxygen/shaders/include/globals.glsl index 7436e5c036..895492a23a 100644 --- a/assets/voxygen/shaders/include/globals.glsl +++ b/assets/voxygen/shaders/include/globals.glsl @@ -17,7 +17,6 @@ uniform u_globals { // 1 - ThirdPerson uint cam_mode; float sprite_render_distance; - float particle_render_distance; }; // Specifies the pattern used in the player dithering diff --git a/assets/voxygen/shaders/particle-frag.glsl b/assets/voxygen/shaders/particle-frag.glsl index 3e2ef6b9e6..3d730f7a50 100644 --- a/assets/voxygen/shaders/particle-frag.glsl +++ b/assets/voxygen/shaders/particle-frag.glsl @@ -34,5 +34,5 @@ void main() { vec3 fog_color = get_sky_color(normalize(f_pos - cam_pos.xyz), time_of_day.x, cam_pos.xyz, f_pos, 0.5, true, clouds); vec3 color = mix(mix(surf_color, fog_color, fog_level), clouds.rgb, clouds.a); - tgt_color = vec4(color, 1.0 - clamp((distance(focus_pos.xy, f_pos.xy) - (particle_render_distance - FADE_DIST)) / FADE_DIST, 0, 1)); + tgt_color = vec4(color, 1.0 - clamp((distance(focus_pos.xy, f_pos.xy) - (1000.0 - FADE_DIST)) / FADE_DIST, 0, 1)); } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index de2ac1649c..20a48df35e 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -269,7 +269,6 @@ pub enum Event { ToggleSmoothPan(bool), AdjustViewDistance(u32), AdjustSpriteRenderDistance(u32), - AdjustParticleRenderDistance(u32), AdjustFigureLoDRenderDistance(u32), AdjustMusicVolume(f32), AdjustSfxVolume(f32), @@ -279,6 +278,7 @@ pub enum Event { ChangeGamma(f32), MapZoom(f64), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), @@ -1911,6 +1911,9 @@ impl Hud { settings_window::Event::ChangeLanguage(language) => { events.push(Event::ChangeLanguage(language)); }, + settings_window::Event::ToggleParticlesEnabled(particles_enabled) => { + events.push(Event::ToggleParticlesEnabled(particles_enabled)); + }, settings_window::Event::ToggleFullscreen => { events.push(Event::ToggleFullscreen); }, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index c0b8e42fff..f9e6b7d17b 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -111,6 +111,8 @@ widget_ids! { cloud_mode_list, fluid_mode_text, fluid_mode_list, + particles_button, + particles_label, fullscreen_button, fullscreen_label, save_window_size_button, @@ -232,6 +234,7 @@ pub enum Event { AdjustFOV(u16), AdjustGamma(f32), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), @@ -1845,7 +1848,6 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.sprite_dist_text, ui); - Text::new(&format!( "{}", self.global_state.settings.graphics.sprite_render_distance @@ -1899,8 +1901,6 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.figure_dist_value, ui); - // TODO: Particle View Distance slider. - // AaMode Text::new(&self.localized_strings.get("hud.settings.antialiasing_mode")) .down_from(state.ids.gamma_slider, 8.0) @@ -2016,11 +2016,34 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ChangeFluidMode(mode_list[clicked])); } + // Particles + Text::new(&self.localized_strings.get("hud.settings.particles")) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.fluid_mode_list, 8.0) + .color(TEXT_COLOR) + .set(state.ids.particles_label, ui); + + let particles_enabled = ToggleButton::new( + self.global_state.settings.graphics.particles_enabled, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .right_from(state.ids.particles_label, 10.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.particles_button, ui); + + if self.global_state.settings.graphics.particles_enabled != particles_enabled { + events.push(Event::ToggleParticlesEnabled(particles_enabled)); + } + // Fullscreen Text::new(&self.localized_strings.get("hud.settings.fullscreen")) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) - .down_from(state.ids.fluid_mode_list, 8.0) + .down_from(state.ids.particles_label, 8.0) .color(TEXT_COLOR) .set(state.ids.fullscreen_label, ui); diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index a3e978a70c..f97890626d 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -1,9 +1,9 @@ pub mod figure; pub mod fluid; +pub mod particle; pub mod postprocess; pub mod skybox; pub mod sprite; -pub mod particle; pub mod terrain; pub mod ui; @@ -30,7 +30,6 @@ gfx_defines! { gamma: [f32; 4] = "gamma", cam_mode: u32 = "cam_mode", sprite_render_distance: f32 = "sprite_render_distance", - particle_render_distance: f32 = "particle_render_distance", } constant Light { @@ -63,7 +62,6 @@ impl Globals { gamma: f32, cam_mode: CameraMode, sprite_render_distance: f32, - particle_render_distance: f32, ) -> Self { Self { view_mat: view_mat.into_col_arrays(), @@ -84,7 +82,6 @@ impl Globals { gamma: [gamma; 4], cam_mode: cam_mode as u32, sprite_render_distance, - particle_render_distance, } } } @@ -107,7 +104,6 @@ impl Default for Globals { 1.0, CameraMode::ThirdPerson, 250.0, - 250.0, ) } } diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index d242239e94..af59657488 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -81,7 +81,7 @@ pub struct SceneData<'a> { pub gamma: f32, pub mouse_smoothing: bool, pub sprite_render_distance: f32, - pub particle_render_distance: f32, + pub particles_enabled: bool, pub figure_lod_render_distance: f32, pub is_aiming: bool, } @@ -377,7 +377,6 @@ impl Scene { scene_data.gamma, self.camera.get_mode(), scene_data.sprite_render_distance as f32 - 20.0, - scene_data.particle_render_distance as f32 - 20.0, )]) .expect("Failed to update global constants"); @@ -437,6 +436,7 @@ impl Scene { self.particle_mgr.render( renderer, + scene_data, &self.globals, &self.lights, &self.shadows, diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 92ef68607e..3a72f92b94 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -48,11 +48,9 @@ impl ParticleMgr { ) .0; - let model = renderer + renderer .create_model(mesh) .expect("Failed to create particle model"); - - model }); let insts = Vec::new(); @@ -73,31 +71,37 @@ impl ParticleMgr { pub fn particle_count_visible(&self) -> usize { self.instances.count() } pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { - let now = Instant::now(); + if scene_data.particles_enabled { + let now = Instant::now(); - // remove dead particles - self.particles.retain(|p| p.alive_until > now); + // remove dead particles + self.particles.retain(|p| p.alive_until > now); - // let zxc = scene_data.particle_render_distance; + self.maintain_body_particles(scene_data); + self.maintain_boost_particles(scene_data); - self.maintain_body_particles(renderer, scene_data); - - self.maintain_boost_particles(renderer, scene_data); + self.upload_particles(renderer); + } else { + self.particles.clear(); + } + } + fn upload_particles(&mut self, renderer: &mut Renderer) { let all_cpu_instances = self .particles .iter() .map(|p| p.instance) .collect::>(); - // TODO: upload just the ones that were created and added to queue, not all of - // them. - self.instances = renderer + // TODO: optimise buffer writes + let gpu_instances = renderer .create_instances(&all_cpu_instances) .expect("Failed to upload particle instances to the GPU!"); + + self.instances = gpu_instances; } - fn maintain_body_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + fn maintain_body_particles(&mut self, scene_data: &SceneData) { let ecs = scene_data.state.ecs(); for (_i, (_entity, body, pos)) in ( &ecs.entities(), @@ -109,31 +113,21 @@ impl ParticleMgr { { match body { Body::Object(object::Body::CampfireLit) => { - self.maintain_campfirelit_particles(renderer, scene_data, pos) + self.maintain_campfirelit_particles(scene_data, pos) }, Body::Object(object::Body::BoltFire) => { - self.maintain_boltfire_particles(renderer, scene_data, pos) + self.maintain_boltfire_particles(scene_data, pos) }, Body::Object(object::Body::BoltFireBig) => { - self.maintain_boltfirebig_particles(renderer, scene_data, pos) + self.maintain_boltfirebig_particles(scene_data, pos) }, - Body::Object(object::Body::Bomb) => { - self.maintain_bomb_particles(renderer, scene_data, pos) - }, - // Body::Object(object::Body::Pouch) => { - // self.maintain_pouch_particles(renderer, scene_data, pos) - // }, + Body::Object(object::Body::Bomb) => self.maintain_bomb_particles(scene_data, pos), _ => {}, } } } - fn maintain_campfirelit_particles( - &mut self, - renderer: &mut Renderer, - scene_data: &SceneData, - pos: &Pos, - ) { + fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); let now = Instant::now(); let mut rng = rand::thread_rng(); @@ -149,12 +143,7 @@ impl ParticleMgr { }); } - fn maintain_boltfire_particles( - &mut self, - renderer: &mut Renderer, - scene_data: &SceneData, - pos: &Pos, - ) { + fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); let now = Instant::now(); let mut rng = rand::thread_rng(); @@ -170,12 +159,7 @@ impl ParticleMgr { }); } - fn maintain_boltfirebig_particles( - &mut self, - renderer: &mut Renderer, - scene_data: &SceneData, - pos: &Pos, - ) { + fn maintain_boltfirebig_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); let now = Instant::now(); let mut rng = rand::thread_rng(); @@ -205,12 +189,7 @@ impl ParticleMgr { }); } - fn maintain_bomb_particles( - &mut self, - renderer: &mut Renderer, - scene_data: &SceneData, - pos: &Pos, - ) { + fn maintain_bomb_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); let now = Instant::now(); let mut rng = rand::thread_rng(); @@ -244,34 +223,7 @@ impl ParticleMgr { }); } - // fn maintain_pouch_particles( - // &mut self, - // renderer: &mut Renderer, - // scene_data: &SceneData, - // pos: &Pos, - // ) { - // let time = scene_data.state.get_time(); - // let now = Instant::now(); - // let mut rng = rand::thread_rng(); - - // let smoke_cpu_insts = vec![ParticleInstance::new( - // time, - // rng.gen(), - // ParticleMode::CampfireSmoke, - // pos.0, - // )]; - - // let smoke_cpu_insts = renderer - // .create_instances(&smoke_cpu_insts) - // .expect("Failed to upload particle instances to the GPU!"); - - // self.particles.push(Particles { - // alive_until: now + Duration::from_secs(1), - // instances: smoke_cpu_insts, - // }); - // } - - fn maintain_boost_particles(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + fn maintain_boost_particles(&mut self, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); let time = state.get_time(); @@ -303,15 +255,18 @@ impl ParticleMgr { pub fn render( &self, renderer: &mut Renderer, + scene_data: &SceneData, globals: &Consts, lights: &Consts, shadows: &Consts, ) { - let model = &self - .model_cache - .get(MODEL_KEY) - .expect("Expected particle model in cache"); + if scene_data.particles_enabled { + let model = &self + .model_cache + .get(MODEL_KEY) + .expect("Expected particle model in cache"); - renderer.render_particles(model, globals, &self.instances, lights, shadows); + renderer.render_particles(model, globals, &self.instances, lights, shadows); + } } } diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index fb620aa0f1..c89ebc6511 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -190,7 +190,6 @@ impl Scene { scene_data.gamma, self.camera.get_mode(), 250.0, - 250.0, )]) { error!(?e, "Renderer failed to update"); } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 86ea35b27a..9c5122cee8 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -788,11 +788,6 @@ impl PlayState for SessionState { sprite_render_distance; global_state.settings.save_to_file_warn(); }, - HudEvent::AdjustParticleRenderDistance(particle_render_distance) => { - global_state.settings.graphics.particle_render_distance = - particle_render_distance; - global_state.settings.save_to_file_warn(); - }, HudEvent::AdjustFigureLoDRenderDistance(figure_lod_render_distance) => { global_state.settings.graphics.figure_lod_render_distance = figure_lod_render_distance; @@ -942,6 +937,10 @@ impl PlayState for SessionState { self.voxygen_i18n.log_missing_entries(); self.hud.update_language(self.voxygen_i18n.clone()); }, + HudEvent::ToggleParticlesEnabled(particles_enabled) => { + global_state.settings.graphics.particles_enabled = particles_enabled; + global_state.settings.save_to_file_warn(); + }, HudEvent::ToggleFullscreen => { global_state .window @@ -1006,11 +1005,7 @@ impl PlayState for SessionState { mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable, sprite_render_distance: global_state.settings.graphics.sprite_render_distance as f32, - particle_render_distance: global_state - .settings - .graphics - .particle_render_distance - as f32, + particles_enabled: global_state.settings.graphics.particles_enabled, figure_lod_render_distance: global_state .settings .graphics @@ -1066,7 +1061,7 @@ impl PlayState for SessionState { mouse_smoothing: settings.gameplay.smooth_pan_enable, sprite_render_distance: settings.graphics.sprite_render_distance as f32, figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32, - particle_render_distance: settings.graphics.particle_render_distance as f32, + particles_enabled: settings.graphics.particles_enabled, is_aiming: self.is_aiming, }; self.scene.render( diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index ef1184c8ee..722b077ae7 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -603,7 +603,7 @@ impl Default for Log { pub struct GraphicsSettings { pub view_distance: u32, pub sprite_render_distance: u32, - pub particle_render_distance: u32, + pub particles_enabled: bool, pub figure_lod_render_distance: u32, pub max_fps: u32, pub fov: u16, @@ -620,7 +620,7 @@ impl Default for GraphicsSettings { Self { view_distance: 10, sprite_render_distance: 150, - particle_render_distance: 150, + particles_enabled: true, figure_lod_render_distance: 250, max_fps: 60, fov: 50, From 5ba4d2682139098b47dbc2dc450d4ff64edef239 Mon Sep 17 00:00:00 2001 From: scott-c Date: Sat, 25 Jul 2020 23:56:50 +0800 Subject: [PATCH 41/71] Update changelog --- CHANGELOG.md | 1 + server/src/sys/sentinel.rs | 3 +- voxygen/src/render/mod.rs | 2 +- voxygen/src/render/renderer.rs | 26 ++++++++------- voxygen/src/scene/particle.rs | 60 +++++++++++++++++++--------------- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c03261b87..078d5790e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Power stat to weapons which affects weapon damage - Add detection of entities under the cursor - Functional group-system with exp-sharing and disabled damage to group members +- Some Campfire, fireball & bomb; particle, light & sound effects. ### Changed diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 6121dbb4a9..8bda2339e8 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,8 +2,7 @@ use super::SysTimer; use common::{ comp::{ Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, - Loadout, Mass, MountState, Mounting, Ori, Player, Pos, - Scale, Stats, Sticky, Vel, + Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, }, msg::EcsCompPacket, sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt}, diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 408251d9ad..317ba97db6 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -18,11 +18,11 @@ pub use self::{ pipelines::{ figure::{BoneData as FigureBoneData, FigurePipeline, Locals as FigureLocals}, fluid::FluidPipeline, + particle::{Instance as ParticleInstance, ParticlePipeline}, postprocess::{ create_mesh as create_pp_mesh, Locals as PostProcessLocals, PostProcessPipeline, }, skybox::{create_mesh as create_skybox_mesh, Locals as SkyboxLocals, SkyboxPipeline}, - particle::{Instance as ParticleInstance, ParticlePipeline}, sprite::{Instance as SpriteInstance, SpritePipeline}, terrain::{Locals as TerrainLocals, TerrainPipeline}, ui::{ diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 80bdb65ba5..1fa5cc6f2c 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -4,7 +4,9 @@ use super::{ instances::Instances, mesh::Mesh, model::{DynamicModel, Model}, - pipelines::{figure, fluid, postprocess, skybox, sprite, particle, terrain, ui, Globals, Light, Shadow}, + pipelines::{ + figure, fluid, particle, postprocess, skybox, sprite, terrain, ui, Globals, Light, Shadow, + }, texture::Texture, AaMode, CloudMode, FluidMode, Pipeline, RenderError, }; @@ -951,17 +953,17 @@ fn create_pipelines( gfx::state::CullFace::Back, )?; - // Construct a pipeline for rendering particles - let particle_pipeline = create_pipeline( - factory, - particle::pipe::new(), - &assets::load_watched::("voxygen.shaders.particle-vert", shader_reload_indicator) - .unwrap(), - &assets::load_watched::("voxygen.shaders.particle-frag", shader_reload_indicator) - .unwrap(), - &include_ctx, - gfx::state::CullFace::Back, - )?; + // Construct a pipeline for rendering particles + let particle_pipeline = create_pipeline( + factory, + particle::pipe::new(), + &assets::load_watched::("voxygen.shaders.particle-vert", shader_reload_indicator) + .unwrap(), + &assets::load_watched::("voxygen.shaders.particle-frag", shader_reload_indicator) + .unwrap(), + &include_ctx, + gfx::state::CullFace::Back, + )?; // Construct a pipeline for rendering UI elements let ui_pipeline = create_pipeline( diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 3a72f92b94..abc3b0eb88 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -34,35 +34,10 @@ const MODEL_KEY: &str = "voxygen.voxel.particle"; impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { - let mut model_cache = HashMap::new(); - - model_cache.entry(MODEL_KEY).or_insert_with(|| { - let offset = Vec3::zero(); - let lod_scale = Vec3::one(); - - let vox = assets::load_expect::(MODEL_KEY); - - let mesh = &Meshable::::generate_mesh( - &Segment::from(vox.as_ref()), - (offset * lod_scale, Vec3::one() / lod_scale), - ) - .0; - - renderer - .create_model(mesh) - .expect("Failed to create particle model"); - }); - - let insts = Vec::new(); - - let instances = renderer - .create_instances(&insts) - .expect("Failed to upload particle instances to the GPU!"); - Self { particles: Vec::new(), - instances, - model_cache, + instances: default_instances(renderer), + model_cache: default_cache(renderer), } } @@ -270,3 +245,34 @@ impl ParticleMgr { } } } + +fn default_instances(renderer: &mut Renderer) -> Instances { + let empty_vec = Vec::new(); + + renderer + .create_instances(&empty_vec) + .expect("Failed to upload particle instances to the GPU!") +} + +fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model> { + let mut model_cache = HashMap::new(); + + model_cache.entry(MODEL_KEY).or_insert_with(|| { + let offset = Vec3::zero(); + let lod_scale = Vec3::one(); + + let vox = assets::load_expect::(MODEL_KEY); + + let mesh = &Meshable::::generate_mesh( + &Segment::from(vox.as_ref()), + (offset * lod_scale, Vec3::one() / lod_scale), + ) + .0; + + renderer + .create_model(mesh) + .expect("Failed to create particle model") + }); + + model_cache +} From 82a0ecad6b4d741034cc41c8938104fee2cbeac5 Mon Sep 17 00:00:00 2001 From: scott-c Date: Fri, 31 Jul 2020 17:32:13 +0800 Subject: [PATCH 42/71] Add zest's changes --- assets/voxygen/shaders/particle-vert.glsl | 92 ++++++++++++++++------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 54fac86749..e9705d5505 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -2,6 +2,7 @@ #include #include +#include in vec3 v_pos; in uint v_col; @@ -43,49 +44,86 @@ mat4 translate(vec3 vec){ ); } +struct Attr { + vec3 offs; + float scale; + vec3 col; +}; + +float lifetime = tick.x - inst_time; + +vec3 linear_motion(vec3 init_offs, vec3 vel) { + return init_offs + vel * lifetime; +} + +vec3 grav_vel(float grav) { + return vec3(0, 0, -grav * lifetime); +} + +float exp_scale(float factor) { + return 1 / (1 - lifetime * factor); +} + void main() { mat4 inst_mat = translate(inst_pos); - float rand1 = gold_noise(vec2(0.0, 0.0), inst_entropy); - float rand2 = gold_noise(vec2(10.0, 10.0), inst_entropy); - float rand3 = gold_noise(vec2(20.0, 20.0), inst_entropy); - float rand4 = gold_noise(vec2(30.0, 30.0), inst_entropy); - float rand5 = gold_noise(vec2(40.0, 40.0), inst_entropy); - float rand6 = gold_noise(vec2(50.0, 50.0), inst_entropy); + float rand0 = hash(vec4(inst_entropy + 0)); + float rand1 = hash(vec4(inst_entropy + 1)); + float rand2 = hash(vec4(inst_entropy + 2)); + float rand3 = hash(vec4(inst_entropy + 3)); + float rand4 = hash(vec4(inst_entropy + 4)); + float rand5 = hash(vec4(inst_entropy + 5)); + float rand6 = hash(vec4(inst_entropy + 6)); + float rand7 = hash(vec4(inst_entropy + 7)); - vec3 inst_vel = vec3(0.0, 0.0, 0.0); - vec3 inst_pos2 = vec3(0.0, 0.0, 0.0); - vec3 inst_col = vec3(1.0, 1.0, 1.0); + Attr attr; if (inst_mode == SMOKE) { - inst_col = vec3(1.0, 1.0, 1.0); - inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 1.0 + rand3); - inst_pos2 = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 1.7 + rand5), + vec3(rand2 * 0.2, rand3 * 0.2, 1.0 + rand4 * 0.5)// + vec3(sin(lifetime), sin(lifetime + 1.5), sin(lifetime * 4) * 0.25) + ), + exp_scale(-0.2), + vec3(1) + ); } else if (inst_mode == FIRE) { - inst_col = vec3(1.0, 1.0 * inst_entropy, 0.0); - inst_vel = vec3(rand1 * 0.2 - 0.1, rand2 * 0.2 - 0.1, 4.0 + rand3); - inst_pos2 = vec3(rand4 * 5.0 - 2.5, rand5 * 5.0 - 2.5, 0.0); + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 0.3), + vec3(rand2 * 0.1, rand3 * 0.1, 2.0 + rand4 * 1.0) + ), + 1.0, + vec3(2, rand5 + 2, 0) + ); } else if (inst_mode == GUN_POWDER_SPARK) { - inst_col = vec3(1.0, 1.0, 0.0); - inst_vel = vec3(rand2 * 2.0 - 1.0, rand1 * 2.0 - 1.0, 5.0 + rand3); - inst_vel -= vec3(0.0, 0.0, earth_gravity * (tick.x - inst_time)); - inst_pos2 = vec3(0.0, 0.0, 0.0); + attr = Attr( + linear_motion( + vec3(rand0, rand1, rand3) * 0.3, + vec3(rand4, rand5, rand6) * 2.0 + grav_vel(earth_gravity) + ), + 1.0, + vec3(3.5, 3 + rand7, 0) + ); } else { - inst_col = vec3(rand1, rand2, rand3); - inst_vel = vec3(rand4, rand5, rand6); - inst_pos2 = vec3(rand1, rand2, rand3); + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 1.7 + rand5), + vec3(rand2 * 0.1, rand3 * 0.1, 1.0 + rand4 * 0.5) + ), + exp_scale(-0.2), + vec3(1) + ); } - f_pos = (inst_mat * vec4((v_pos + inst_pos2) * SCALE, 1)).xyz; - - f_pos += inst_vel * (tick.x - inst_time); + f_pos = (inst_mat * vec4(v_pos * attr.scale * SCALE + attr.offs, 1)).xyz; // First 3 normals are negative, next 3 are positive vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); f_norm = (inst_mat * vec4(normals[(v_norm_ao >> 0) & 0x7u], 0)).xyz; vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; - f_col = srgb_to_linear(col) * srgb_to_linear(inst_col); + f_col = srgb_to_linear(col) * srgb_to_linear(attr.col); f_ao = float((v_norm_ao >> 3) & 0x3u) / 4.0; f_light = 1.0; @@ -94,4 +132,4 @@ void main() { all_mat * vec4(f_pos, 1); gl_Position.z = -1000.0 / (gl_Position.z + 10000.0); -} +} \ No newline at end of file From 1a263834e7bbeff7e22928b0eefa34ca11d6a6d9 Mon Sep 17 00:00:00 2001 From: scott-c Date: Fri, 31 Jul 2020 17:34:26 +0800 Subject: [PATCH 43/71] Add campfire command --- common/src/cmd.rs | 4 ++++ server/src/cmd.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 6d8ed3af46..e128901f46 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -38,6 +38,7 @@ pub enum ChatCommand { Adminify, Alias, Build, + Campfire, Debug, DebugColumn, Dummy, @@ -79,6 +80,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, ChatCommand::Alias, ChatCommand::Build, + ChatCommand::Campfire, ChatCommand::Debug, ChatCommand::DebugColumn, ChatCommand::Dummy, @@ -185,6 +187,7 @@ impl ChatCommand { ), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", Admin), + ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Admin), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", Admin), ChatCommand::DebugColumn => cmd( vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], @@ -365,6 +368,7 @@ impl ChatCommand { ChatCommand::Adminify => "adminify", ChatCommand::Alias => "alias", ChatCommand::Build => "build", + ChatCommand::Campfire => "campfire", ChatCommand::Debug => "debug", ChatCommand::DebugColumn => "debug_column", ChatCommand::Dummy => "dummy", diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b580cfbd98..3d8d3bef72 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -65,6 +65,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Adminify => handle_adminify, ChatCommand::Alias => handle_alias, ChatCommand::Build => handle_build, + ChatCommand::Campfire => handle_spawn_campfire, ChatCommand::Debug => handle_debug, ChatCommand::DebugColumn => handle_debug_column, ChatCommand::Dummy => handle_spawn_training_dummy, @@ -664,6 +665,47 @@ fn handle_spawn_training_dummy( } } +fn handle_spawn_campfire( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + match server.state.read_component_cloned::(target) { + Some(pos) => { + let vel = Vec3::new( + rand::thread_rng().gen_range(-2.0, 3.0), + rand::thread_rng().gen_range(-2.0, 3.0), + 10.0, + ); + + let body = comp::Body::Object(comp::object::Body::CampfireLit); + + let mut stats = comp::Stats::new("Campfire".to_string(), body); + + // Level 0 will prevent exp gain from kill + stats.level.set_level(0); + + server + .state + .create_npc(pos, stats, comp::Loadout::default(), body) + .with(comp::Vel(vel)) + .with(comp::MountState::Unmounted) + .build(); + + server.notify_client( + client, + ChatType::CommandInfo.server_msg("Spawned a campfire"), + ); + }, + None => server.notify_client( + client, + ChatType::CommandError.server_msg("You have no position!"), + ), + } +} + fn handle_players( server: &mut Server, client: EcsEntity, From 3f7667352dba865b74ac6cb2143642b2e12360ee Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Jul 2020 15:00:58 +0100 Subject: [PATCH 44/71] Better hash RNG --- assets/voxygen/shaders/include/random.glsl | 2 +- assets/voxygen/shaders/include/sky.glsl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/voxygen/shaders/include/random.glsl b/assets/voxygen/shaders/include/random.glsl index 240d17e50a..7d217d858e 100644 --- a/assets/voxygen/shaders/include/random.glsl +++ b/assets/voxygen/shaders/include/random.glsl @@ -1,5 +1,5 @@ float hash(vec4 p) { - p = fract(p * 0.3183099 + 0.1); + p = fract(p * 0.3183099 + 0.1) - fract(p + 23.22121); p *= 17.0; return (fract(p.x * p.y * p.z * p.w * (p.x + p.y + p.z + p.w)) - 0.5) * 2.0; } diff --git a/assets/voxygen/shaders/include/sky.glsl b/assets/voxygen/shaders/include/sky.glsl index 6fd726230b..fe12499086 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -94,10 +94,10 @@ float is_star_at(vec3 dir) { vec3 pos = (floor(dir * star_scale) - 0.5) / star_scale; // Noisy offsets - pos += (3.0 / star_scale) * rand_perm_3(pos); + pos += (3.0 / star_scale) * (1.0 + hash(pos.yxzz) * 0.85); // Find distance to fragment - float dist = length(normalize(pos) - dir); + float dist = length(pos - dir); // Star threshold if (dist < 0.0015) { From 7c31baef6f767794d0f8b1663e89a7386cc426f7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Jul 2020 18:16:20 +0100 Subject: [PATCH 45/71] Added outcome system, sound effects --- assets/voxygen/shaders/particle-vert.glsl | 2 +- client/src/lib.rs | 5 ++++ common/src/lib.rs | 1 + common/src/msg/server.rs | 2 ++ common/src/outcome.rs | 30 +++++++++++++++++++++++ server/src/events/entity_creation.rs | 8 +++++- server/src/events/entity_manipulation.rs | 24 ++++++------------ server/src/lib.rs | 2 ++ server/src/sys/entity_sync.rs | 26 +++++++++++++++++++- voxygen/src/audio/sfx/mod.rs | 13 ++++++++++ voxygen/src/session.rs | 11 ++++++++- 11 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 common/src/outcome.rs diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index e9705d5505..70580be584 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -132,4 +132,4 @@ void main() { all_mat * vec4(f_pos, 1); gl_Position.z = -1000.0 / (gl_Position.z + 10000.0); -} \ No newline at end of file +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 404c2a1686..08109865db 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -30,6 +30,7 @@ use common::{ sync::{Uid, UidAllocator, WorldSyncExt}, terrain::{block::Block, TerrainChunk, TerrainChunkSize}, vol::RectVolSize, + outcome::Outcome, }; use futures_executor::block_on; use futures_timer::Delay; @@ -66,6 +67,7 @@ pub enum Event { InventoryUpdated(InventoryUpdateEvent), Notification(Notification), SetViewDistance(u32), + Outcome(Outcome), } pub struct Client { @@ -1229,6 +1231,9 @@ impl Client { self.view_distance = Some(vd); frontend_events.push(Event::SetViewDistance(vd)); }, + ServerMsg::Outcomes(outcomes) => frontend_events.extend(outcomes + .into_iter() + .map(Event::Outcome)), } } } diff --git a/common/src/lib.rs b/common/src/lib.rs index fb04ff28b1..3bddacde79 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -24,6 +24,7 @@ pub mod generation; pub mod loadout_builder; pub mod msg; pub mod npc; +pub mod outcome; pub mod path; pub mod ray; pub mod recipe; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 156c18fd61..d6d55e46e8 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -2,6 +2,7 @@ use super::{ClientState, EcsCompPacket}; use crate::{ character::CharacterItem, comp, + outcome::Outcome, recipe::RecipeBook, state, sync, sync::Uid, @@ -120,6 +121,7 @@ pub enum ServerMsg { /// Send a popup notification such as "Waypoint Saved" Notification(Notification), SetViewDistance(u32), + Outcomes(Vec), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/common/src/outcome.rs b/common/src/outcome.rs new file mode 100644 index 0000000000..9f047bcc26 --- /dev/null +++ b/common/src/outcome.rs @@ -0,0 +1,30 @@ +use crate::comp; +use serde::{Deserialize, Serialize}; +use vek::*; + +/// An outcome represents the final result of an instantaneous event. It implies that said event has +/// already occurred. It is not a request for that event to occur, nor is it something that may be +/// cancelled or otherwise altered. Its primary purpose is to act as something for frontends (both +/// server and client) to listen to in order to receive feedback about events in the world. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Outcome { + Explosion { + pos: Vec3, + power: f32, + }, + ProjectileShot { + pos: Vec3, + body: comp::Body, + vel: Vec3, + }, +} + + +impl Outcome { + pub fn get_pos(&self) -> Option> { + match self { + Outcome::Explosion { pos, .. } => Some(*pos), + Outcome::ProjectileShot { pos, .. } => Some(*pos), + } + } +} diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index a62c0b342c..0d9a8ff61a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -5,6 +5,7 @@ use common::{ Projectile, Scale, Stats, Vel, WaypointArea, }, util::Dir, + outcome::Outcome, }; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -89,10 +90,15 @@ pub fn handle_shoot( .expect("Failed to fetch entity") .0; + let vel = *dir * 100.0; + + // Add an outcome + state.ecs().write_resource::>().push(Outcome::ProjectileShot { pos, body, vel }); + // TODO: Player height pos.z += 1.2; - let mut builder = state.create_projectile(Pos(pos), Vel(*dir * 100.0), body, projectile); + let mut builder = state.create_projectile(Pos(pos), Vel(vel), body, projectile); if let Some(light) = light { builder = builder.with(light) } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 0a73123a28..d87e61ea08 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -6,6 +6,7 @@ use common::{ HealthChange, HealthSource, Player, Pos, Stats, }, msg::{PlayerListUpdate, ServerMsg}, + outcome::Outcome, state::BlockChange, sync::{Uid, UidAllocator, WorldSyncExt}, sys::combat::BLOCK_ANGLE, @@ -277,25 +278,16 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { } } -pub fn handle_explosion( - server: &Server, - pos: Vec3, - power: f32, - owner: Option, - friendly_damage: bool, -) { - // Go through all other entities - let hit_range = 3.0 * power; +pub fn handle_explosion(server: &Server, pos: Vec3, power: f32, owner: Option) { let ecs = &server.state.ecs(); - let owner_entity = owner.and_then(|uid| { - ecs.read_resource::() - .retrieve_entity_internal(uid.into()) - }); - let groups = ecs.read_storage::(); + // Add an outcome + ecs.write_resource::>() + .push(Outcome::Explosion { pos, power }); - for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in ( - &ecs.entities(), + // Go through all other entities + let hit_range = 3.0 * power; + for (pos_b, ori_b, character_b, stats_b, loadout_b) in ( &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), diff --git a/server/src/lib.rs b/server/src/lib.rs index 7d27b933ab..1447793c98 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -34,6 +34,7 @@ use common::{ comp::{self, ChatType}, event::{EventBus, ServerEvent}, msg::{ClientState, ServerInfo, ServerMsg}, + outcome::Outcome, recipe::default_recipe_book, state::{State, TimeOfDay}, sync::WorldSyncExt, @@ -118,6 +119,7 @@ impl Server { state .ecs_mut() .insert(comp::AdminList(settings.admins.clone())); + state.ecs_mut().insert(Vec::::new()); // System timers for performance monitoring state.ecs_mut().insert(sys::EntitySyncTimer::default()); diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index d4bbf62d20..6e4a5fa897 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -7,15 +7,19 @@ use crate::{ Tick, }; use common::{ - comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel}, + comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel, Player}, msg::ServerMsg, + outcome::Outcome, region::{Event as RegionEvent, RegionMap}, state::TimeOfDay, sync::{CompSyncPackage, Uid}, + vol::RectVolSize, + terrain::TerrainChunkSize, }; use specs::{ Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage, }; +use vek::*; /// This system will send physics updates to the client pub struct Sys; @@ -33,6 +37,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Inventory>, ReadStorage<'a, RegionSubscription>, + ReadStorage<'a, Player>, WriteStorage<'a, Last>, WriteStorage<'a, Last>, WriteStorage<'a, Last>, @@ -40,6 +45,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, ForceUpdate>, WriteStorage<'a, InventoryUpdate>, Write<'a, DeletedEntities>, + Write<'a, Vec>, TrackedComps<'a>, ReadTrackers<'a>, ); @@ -58,6 +64,7 @@ impl<'a> System<'a> for Sys { orientations, inventories, subscriptions, + players, mut last_pos, mut last_vel, mut last_ori, @@ -65,6 +72,7 @@ impl<'a> System<'a> for Sys { mut force_updates, mut inventory_updates, mut deleted_entities, + mut outcomes, tracked_comps, trackers, ): Self::SystemData, @@ -316,6 +324,22 @@ impl<'a> System<'a> for Sys { )); } + // Sync outcomes + for (client, player, pos) in (&mut clients, &players, positions.maybe()).join() { + let is_near = |o_pos: Vec3| pos + .zip_with(player.view_distance, |pos, vd| pos.0.xy().distance_squared(o_pos.xy()) < (vd as f32 * TerrainChunkSize::RECT_SIZE.x as f32).powf(2.0)); + + let outcomes = outcomes + .iter() + .filter(|o| o.get_pos().and_then(&is_near).unwrap_or(true)) + .cloned() + .collect::>(); + if outcomes.len() > 0 { + client.notify(ServerMsg::Outcomes(outcomes)); + } + } + outcomes.clear(); + // Remove all force flags. force_updates.clear(); inventory_updates.clear(); diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index b76c287cdb..669065f1cc 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -93,6 +93,7 @@ use common::{ }, event::EventBus, state::State, + outcome::Outcome, }; use event_mapper::SfxEventMapper; use hashbrown::HashMap; @@ -188,6 +189,18 @@ impl From<&InventoryUpdateEvent> for SfxEvent { } } +impl TryFrom for SfxEventItem { + type Error = (); + + fn try_from(outcome: Outcome) -> Result { + match outcome { + Outcome::Explosion { pos, power } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), Some((power / 10.0).min(1.0)))), + Outcome::ProjectileShot { pos, .. } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), None)), + _ => Err(()), + } + } +} + #[derive(Deserialize)] pub struct SfxTriggerItem { pub files: Vec, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 9c5122cee8..abf55456bc 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -26,7 +26,7 @@ use common::{ vol::ReadVol, }; use specs::{Join, WorldExt}; -use std::{cell::RefCell, rc::Rc, time::Duration}; +use std::{cell::RefCell, rc::Rc, time::Duration, convert::TryFrom}; use tracing::{error, info}; use vek::*; @@ -158,6 +158,15 @@ impl SessionState { global_state.settings.graphics.view_distance = vd; global_state.settings.save_to_file_warn(); }, + client::Event::Outcome(outcome) => { + if let Ok(sfx_event_item) = SfxEventItem::try_from(outcome) { + client + .state() + .ecs() + .read_resource::>() + .emit_now(sfx_event_item); + } + }, } } From 8547cdd6810f9d912f454fa339e86f4831244f61 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Jul 2020 23:25:23 +0100 Subject: [PATCH 46/71] Added outcome sound effects, fixed directional sound, particle outcomes --- assets/voxygen/audio/sfx.ron | 14 ++++- voxygen/src/audio/channel.rs | 15 +++-- voxygen/src/audio/mod.rs | 59 +++++++------------ .../src/audio/sfx/event_mapper/combat/mod.rs | 15 +++-- voxygen/src/audio/sfx/event_mapper/mod.rs | 6 +- .../audio/sfx/event_mapper/movement/mod.rs | 14 ++--- .../audio/sfx/event_mapper/progression/mod.rs | 7 ++- voxygen/src/audio/sfx/mod.rs | 41 ++++++------- voxygen/src/scene/camera.rs | 4 ++ voxygen/src/scene/mod.rs | 8 ++- voxygen/src/scene/particle.rs | 21 ++++++- voxygen/src/scene/simple.rs | 1 + voxygen/src/session.rs | 33 ++++++----- 13 files changed, 136 insertions(+), 102 deletions(-) diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index c2725ba3cd..85ff1b92af 100644 --- a/assets/voxygen/audio/sfx.ron +++ b/assets/voxygen/audio/sfx.ron @@ -106,6 +106,18 @@ "voxygen.audio.sfx.inventory.consumable.food", ], threshold: 0.3, - ) + ), + Explosion: ( + files: [ + "voxygen.audio.sfx.glider_open", + ], + threshold: 0.5, + ), + ProjectileShot: ( + files: [ + "voxygen.audio.sfx.glider_open", + ], + threshold: 0.5, + ), } ) diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 8a88923f95..6662e0efd0 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -16,7 +16,10 @@ //! the channel capacity has been reached and all channels are occupied, a //! warning is logged, and no sound is played. -use crate::audio::fader::{FadeDirection, Fader}; +use crate::audio::{ + fader::{FadeDirection, Fader}, + Listener, +}; use rodio::{Device, Sample, Sink, Source, SpatialSink}; use vek::*; @@ -163,11 +166,13 @@ impl SfxChannel { pub fn is_done(&self) -> bool { self.sink.empty() } - pub fn set_emitter_position(&mut self, pos: [f32; 3]) { self.sink.set_emitter_position(pos); } + pub fn set_pos(&mut self, pos: Vec3) { self.pos = pos; } - pub fn set_left_ear_position(&mut self, pos: [f32; 3]) { self.sink.set_left_ear_position(pos); } + pub fn update(&mut self, listener: &Listener) { + const FALLOFF: f32 = 0.13; - pub fn set_right_ear_position(&mut self, pos: [f32; 3]) { - self.sink.set_right_ear_position(pos); + self.sink.set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array()); + self.sink.set_left_ear_position(listener.ear_left_rpos.into_array()); + self.sink.set_right_ear_position(listener.ear_right_rpos.into_array()); } } diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 9c7b022dc4..5af08f8dc6 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -16,7 +16,14 @@ use cpal::traits::DeviceTrait; use rodio::{source::Source, Decoder, Device}; use vek::*; -const FALLOFF: f32 = 0.13; +#[derive(Default, Clone)] +pub struct Listener { + pos: Vec3, + ori: Vec3, + + ear_left_rpos: Vec3, + ear_right_rpos: Vec3, +} /// Holds information about the system audio devices and internal channels used /// for sfx and music playback. An instance of `AudioFrontend` is used by @@ -34,11 +41,7 @@ pub struct AudioFrontend { sfx_volume: f32, music_volume: f32, - listener_pos: Vec3, - listener_ori: Vec3, - - listener_ear_left: Vec3, - listener_ear_right: Vec3, + listener: Listener, } impl AudioFrontend { @@ -63,10 +66,8 @@ impl AudioFrontend { sfx_channels, sfx_volume: 1.0, music_volume: 1.0, - listener_pos: Vec3::zero(), - listener_ori: Vec3::zero(), - listener_ear_left: Vec3::zero(), - listener_ear_right: Vec3::zero(), + + listener: Listener::default(), } } @@ -81,10 +82,7 @@ impl AudioFrontend { sfx_channels: Vec::new(), sfx_volume: 1.0, music_volume: 1.0, - listener_pos: Vec3::zero(), - listener_ori: Vec3::zero(), - listener_ear_left: Vec3::zero(), - listener_ear_right: Vec3::zero(), + listener: Listener::default(), } } @@ -146,20 +144,15 @@ impl AudioFrontend { /// Play (once) an sfx file by file path at the give position and volume pub fn play_sfx(&mut self, sound: &str, pos: Vec3, vol: Option) { if self.audio_device.is_some() { - let calc_pos = ((pos - self.listener_pos) * FALLOFF).into_array(); - let sound = self .sound_cache .load_sound(sound) .amplify(vol.unwrap_or(1.0)); - let left_ear = self.listener_ear_left.into_array(); - let right_ear = self.listener_ear_right.into_array(); - + let listener = self.listener.clone(); if let Some(channel) = self.get_sfx_channel() { - channel.set_emitter_position(calc_pos); - channel.set_left_ear_position(left_ear); - channel.set_right_ear_position(right_ear); + channel.set_pos(pos); + channel.update(&listener); channel.play(sound); } } @@ -174,27 +167,17 @@ impl AudioFrontend { } } - pub fn set_listener_pos(&mut self, pos: &Vec3, ori: &Vec3) { - self.listener_pos = *pos; - self.listener_ori = ori.normalized(); + pub fn set_listener_pos(&mut self, pos: Vec3, ori: Vec3) { + self.listener.pos = pos; + self.listener.ori = ori.normalized(); let up = Vec3::new(0.0, 0.0, 1.0); - - let pos_left = up.cross(self.listener_ori).normalized(); - let pos_right = self.listener_ori.cross(up).normalized(); - - self.listener_ear_left = pos_left; - self.listener_ear_right = pos_right; + self.listener.ear_left_rpos = up.cross(self.listener.ori).normalized(); + self.listener.ear_right_rpos = -up.cross(self.listener.ori).normalized(); for channel in self.sfx_channels.iter_mut() { if !channel.is_done() { - // TODO: Update this to correctly determine the updated relative position of - // the SFX emitter when the player (listener) moves - // channel.set_emitter_position( - // ((channel.pos - self.listener_pos) * FALLOFF).into_array(), - // ); - channel.set_left_ear_position(pos_left.into_array()); - channel.set_right_ear_position(pos_right.into_array()); + channel.update(&self.listener); } } } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs index eeeafca25d..5a81800930 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs @@ -1,6 +1,9 @@ /// EventMapper::Combat watches the combat states of surrounding entities' and /// emits sfx related to weapons and attacks/abilities -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}, + scene::Camera, +}; use super::EventMapper; @@ -15,7 +18,6 @@ use common::{ use hashbrown::HashMap; use specs::{Entity as EcsEntity, Join, WorldExt}; use std::time::{Duration, Instant}; -use vek::*; #[derive(Clone)] struct PreviousEntityState { @@ -39,16 +41,13 @@ pub struct CombatEventMapper { } impl EventMapper for CombatEventMapper { - fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) { + fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); let mut sfx_emitter = sfx_event_bus.emitter(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + let cam_pos = camera.dependents().cam_pos; for (entity, pos, loadout, character) in ( &ecs.entities(), @@ -58,7 +57,7 @@ impl EventMapper for CombatEventMapper { ) .join() .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR + (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR }) { if let Some(character) = character { diff --git a/voxygen/src/audio/sfx/event_mapper/mod.rs b/voxygen/src/audio/sfx/event_mapper/mod.rs index 1628185479..4ac331fa86 100644 --- a/voxygen/src/audio/sfx/event_mapper/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/mod.rs @@ -8,10 +8,11 @@ use combat::CombatEventMapper; use movement::MovementEventMapper; use progression::ProgressionEventMapper; +use crate::scene::Camera; use super::SfxTriggers; trait EventMapper { - fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers); + fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers); } pub struct SfxEventMapper { @@ -33,10 +34,11 @@ impl SfxEventMapper { &mut self, state: &State, player_entity: specs::Entity, + camera: &Camera, triggers: &SfxTriggers, ) { for mapper in &mut self.mappers { - mapper.maintain(state, player_entity, triggers); + mapper.maintain(state, player_entity, camera, triggers); } } } diff --git a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs index a884066274..da6d29edc7 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs @@ -2,7 +2,10 @@ /// and triggers sfx related to running, climbing and gliding, at a volume /// proportionate to the extity's size use super::EventMapper; -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}, + scene::Camera, +}; use common::{ comp::{Body, CharacterState, PhysicsState, Pos, Vel}, event::EventBus, @@ -35,16 +38,13 @@ pub struct MovementEventMapper { } impl EventMapper for MovementEventMapper { - fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) { + fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); let mut sfx_emitter = sfx_event_bus.emitter(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + let cam_pos = camera.dependents().cam_pos; for (entity, pos, vel, body, physics, character) in ( &ecs.entities(), @@ -56,7 +56,7 @@ impl EventMapper for MovementEventMapper { ) .join() .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR + (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR }) { if let Some(character) = character { diff --git a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs index 0b845e03a9..64c21b5303 100644 --- a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs @@ -2,7 +2,10 @@ /// and triggers sfx for gaining experience and levelling up use super::EventMapper; -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers}, + scene::Camera, +}; use common::{comp::Stats, event::EventBus, state::State}; use specs::WorldExt; @@ -23,7 +26,7 @@ pub struct ProgressionEventMapper { impl EventMapper for ProgressionEventMapper { #[allow(clippy::op_ref)] // TODO: Pending review in #587 - fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers) { + fn maintain(&mut self, state: &State, player_entity: specs::Entity, _camera: &Camera, triggers: &SfxTriggers) { let ecs = state.ecs(); let next_state = ecs.read_storage::().get(player_entity).map_or( diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 669065f1cc..d2bb328494 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -83,17 +83,17 @@ mod event_mapper; -use crate::audio::AudioFrontend; +use crate::{audio::AudioFrontend, scene::Camera}; use common::{ assets, comp::{ - item::{ItemKind, ToolCategory}, + item::{Consumable, ItemKind, ToolCategory}, CharacterAbilityType, InventoryUpdateEvent, Ori, Pos, }, event::EventBus, - state::State, outcome::Outcome, + state::State, }; use event_mapper::SfxEventMapper; use hashbrown::HashMap; @@ -147,6 +147,8 @@ pub enum SfxEvent { Wield(ToolCategory), Unwield(ToolCategory), Inventory(SfxInventoryEvent), + Explosion, + ProjectileShot, } #[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)] @@ -189,13 +191,19 @@ impl From<&InventoryUpdateEvent> for SfxEvent { } } -impl TryFrom for SfxEventItem { +impl<'a> TryFrom<&'a Outcome> for SfxEventItem { type Error = (); - fn try_from(outcome: Outcome) -> Result { + fn try_from(outcome: &'a Outcome) -> Result { match outcome { - Outcome::Explosion { pos, power } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), Some((power / 10.0).min(1.0)))), - Outcome::ProjectileShot { pos, .. } => Ok(Self::new(SfxEvent::GliderOpen, Some(pos), None)), + Outcome::Explosion { pos, power } => Ok(Self::new( + SfxEvent::Explosion, + Some(*pos), + Some((*power / 10.0).min(1.0)), + )), + Outcome::ProjectileShot { pos, .. } => { + Ok(Self::new(SfxEvent::ProjectileShot, Some(*pos), None)) + }, _ => Err(()), } } @@ -237,36 +245,25 @@ impl SfxMgr { audio: &mut AudioFrontend, state: &State, player_entity: specs::Entity, + camera: &Camera, ) { if !audio.sfx_enabled() { return; } self.event_mapper - .maintain(state, player_entity, &self.triggers); + .maintain(state, player_entity, camera, &self.triggers); let ecs = state.ecs(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); - - let player_ori = *ecs - .read_storage::() - .get(player_entity) - .copied() - .unwrap_or_default() - .0; - - audio.set_listener_pos(&player_position, &player_ori); + audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir); let events = ecs.read_resource::>().recv_all(); for event in events { let position = match event.pos { Some(pos) => pos, - _ => player_position, + _ => camera.dependents().cam_pos, }; if let Some(item) = self.triggers.get_trigger(&event.sfx) { diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 265688dc82..65aaa7bbe2 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -29,6 +29,7 @@ pub struct Dependents { pub view_mat: Mat4, pub proj_mat: Mat4, pub cam_pos: Vec3, + pub cam_dir: Vec3, } pub struct Camera { @@ -67,6 +68,7 @@ impl Camera { view_mat: Mat4::identity(), proj_mat: Mat4::identity(), cam_pos: Vec3::zero(), + cam_dir: Vec3::unit_y(), }, } } @@ -104,6 +106,8 @@ impl Camera { // TODO: Make this more efficient. self.dependents.cam_pos = Vec3::from(self.dependents.view_mat.inverted() * Vec4::unit_w()); + + self.dependents.cam_dir = Vec3::from(self.dependents.view_mat.inverted() * -Vec4::unit_z()); } pub fn frustum(&self) -> Frustum { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index af59657488..83b827799e 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -4,7 +4,7 @@ pub mod particle; pub mod simple; pub mod terrain; -use self::{ +pub use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, particle::ParticleMgr, @@ -135,6 +135,9 @@ impl Scene { /// Get a reference to the scene's particle manager. pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr } + /// Get a mutable reference to the scene's particle manager. + pub fn particle_mgr_mut(&mut self) -> &mut ParticleMgr { &mut self.particle_mgr } + /// Get a reference to the scene's figure manager. pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr } @@ -273,6 +276,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); // Update chunk loaded distance smoothly for nice shader fog @@ -401,7 +405,7 @@ impl Scene { // Maintain audio self.sfx_mgr - .maintain(audio, scene_data.state, scene_data.player_entity); + .maintain(audio, scene_data.state, scene_data.player_entity, &self.camera); self.music_mgr.maintain(audio, scene_data.state); } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index abc3b0eb88..68aed3c937 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -10,13 +10,14 @@ use common::{ assets, comp::{object, Body, CharacterState, Pos}, figure::Segment, + outcome::Outcome, }; use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::Rng; use specs::{Join, WorldExt}; use std::time::{Duration, Instant}; -use vek::Vec3; +use vek::*; struct Particles { alive_until: Instant, // created_at + lifespan @@ -45,6 +46,24 @@ impl ParticleMgr { pub fn particle_count_visible(&self) -> usize { self.instances.count() } + pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { + let time = scene_data.state.get_time(); + let now = Instant::now(); + let mut rng = rand::thread_rng(); + + match outcome { + Outcome::Explosion { pos, power } => { + for _ in 0..64 { + self.particles.push(Particles { + alive_until: now + Duration::from_secs(4), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power)), + }); + } + }, + _ => {}, + } + } + pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { if scene_data.particles_enabled { let now = Instant::now(); diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index c89ebc6511..259419fee6 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -171,6 +171,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); const VD: f32 = 115.0; // View Distance const TIME: f64 = 43200.0; // 12 hours*3600 seconds diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index abf55456bc..a6a417c9f3 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -24,6 +24,7 @@ use common::{ terrain::{Block, BlockKind}, util::Dir, vol::ReadVol, + outcome::Outcome, }; use specs::{Join, WorldExt}; use std::{cell::RefCell, rc::Rc, time::Duration, convert::TryFrom}; @@ -100,7 +101,7 @@ impl SessionState { } /// Tick the session (and the client attached to it). - fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result { + fn tick(&mut self, dt: Duration, global_state: &mut GlobalState, outcomes: &mut Vec) -> Result { self.inputs.tick(dt); let mut client = self.client.borrow_mut(); @@ -158,15 +159,7 @@ impl SessionState { global_state.settings.graphics.view_distance = vd; global_state.settings.save_to_file_warn(); }, - client::Event::Outcome(outcome) => { - if let Ok(sfx_event_item) = SfxEventItem::try_from(outcome) { - client - .state() - .ecs() - .read_resource::>() - .emit_now(sfx_event_item); - } - }, + client::Event::Outcome(outcome) => outcomes.push(outcome), } } @@ -218,7 +211,7 @@ impl PlayState for SessionState { .camera_mut() .compute_dependents(&*self.client.borrow().state().terrain()); let camera::Dependents { - view_mat, cam_pos, .. + cam_pos, cam_dir, .. } = self.scene.camera().dependents(); let (is_aiming, aim_dir_offset) = { @@ -241,8 +234,6 @@ impl PlayState for SessionState { }; self.is_aiming = is_aiming; - let cam_dir: Vec3 = Vec3::from(view_mat.inverted() * -Vec4::unit_z()); - // Check to see whether we're aiming at anything let (build_pos, select_pos, target_entity) = under_cursor(&self.client.borrow(), cam_pos, cam_dir); @@ -642,10 +633,12 @@ impl PlayState for SessionState { self.inputs.climb = self.key_state.climb(); + let mut outcomes = Vec::new(); + // Runs if either in a multiplayer server or the singleplayer server is unpaused if !global_state.paused() { // Perform an in-game tick. - match self.tick(global_state.clock.get_avg_delta(), global_state) { + match self.tick(global_state.clock.get_avg_delta(), global_state, &mut outcomes) { Ok(TickAction::Continue) => {}, // Do nothing Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu Err(err) => { @@ -1030,6 +1023,18 @@ impl PlayState for SessionState { &mut global_state.audio, &scene_data, ); + + // Process outcomes from client + for outcome in outcomes { + if let Ok(sfx_event_item) = SfxEventItem::try_from(&outcome) { + client + .state() + .ecs() + .read_resource::>() + .emit_now(sfx_event_item); + } + self.scene.particle_mgr_mut().handle_outcome(&outcome, &scene_data); + } } } From a924f9694d302361bb1beb1bc7abaa2ecff16138 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Aug 2020 12:56:12 +0100 Subject: [PATCH 47/71] Explosion sound and event lights --- assets/voxygen/audio/sfx.ron | 4 +-- assets/voxygen/audio/sfx/explosion.wav | 3 ++ assets/voxygen/shaders/particle-vert.glsl | 10 ++++++ voxygen/src/audio/sfx/mod.rs | 2 +- voxygen/src/render/pipelines/mod.rs | 5 +++ voxygen/src/render/pipelines/particle.rs | 7 ++-- voxygen/src/scene/mod.rs | 41 ++++++++++++++++++++--- voxygen/src/scene/particle.rs | 8 ++++- voxygen/src/session.rs | 2 +- 9 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 assets/voxygen/audio/sfx/explosion.wav diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index 85ff1b92af..e81459a2df 100644 --- a/assets/voxygen/audio/sfx.ron +++ b/assets/voxygen/audio/sfx.ron @@ -109,9 +109,9 @@ ), Explosion: ( files: [ - "voxygen.audio.sfx.glider_open", + "voxygen.audio.sfx.explosion", ], - threshold: 0.5, + threshold: 0.2, ), ProjectileShot: ( files: [ diff --git a/assets/voxygen/audio/sfx/explosion.wav b/assets/voxygen/audio/sfx/explosion.wav new file mode 100644 index 0000000000..f7269ac71c --- /dev/null +++ b/assets/voxygen/audio/sfx/explosion.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d7f0bb0a0865d45e98d107c1d24a448aaeeced9c37db9f9e472ab3e1bd2eb03 +size 604946 diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 70580be584..49d2fd86ab 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -31,6 +31,7 @@ float gold_noise(in vec2 xy, in float seed){ const int SMOKE = 0; const int FIRE = 1; const int GUN_POWDER_SPARK = 2; +const int SHRAPNEL = 3; // meters per second const float earth_gravity = 9.807; @@ -105,6 +106,15 @@ void main() { 1.0, vec3(3.5, 3 + rand7, 0) ); + } else if (inst_mode == SHRAPNEL) { + attr = Attr( + linear_motion( + vec3(0), + vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) + ), + 3.0 + rand0, + vec3(0.6 + rand7 * 0.4) + ); } else { attr = Attr( linear_motion( diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index d2bb328494..6a4e0f8855 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -199,7 +199,7 @@ impl<'a> TryFrom<&'a Outcome> for SfxEventItem { Outcome::Explosion { pos, power } => Ok(Self::new( SfxEvent::Explosion, Some(*pos), - Some((*power / 10.0).min(1.0)), + Some((*power / 2.5).min(1.5)), )), Outcome::ProjectileShot { pos, .. } => { Ok(Self::new(SfxEvent::ProjectileShot, Some(*pos), None)) diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index f97890626d..442b31be32 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -117,6 +117,11 @@ impl Light { } pub fn get_pos(&self) -> Vec3 { Vec3::new(self.pos[0], self.pos[1], self.pos[2]) } + + pub fn with_strength(mut self, strength: f32) -> Self { + self.col = (Vec4::::from(self.col) * strength).into_array(); + self + } } impl Default for Light { diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 1f467c638e..e8e222e224 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -83,9 +83,10 @@ impl Vertex { } pub enum ParticleMode { - CampfireSmoke, - CampfireFire, - GunPowderSpark, + CampfireSmoke = 0, + CampfireFire = 1, + GunPowderSpark = 2, + Shrapnel = 3, } impl ParticleMode { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 83b827799e..5ee7f0257b 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -21,9 +21,10 @@ use crate::{ use anim::character::SkeletonAttr; use common::{ comp, - state::State, + state::{State, DeltaTime}, terrain::{BlockKind, TerrainChunk}, vol::ReadVol, + outcome::Outcome, }; use specs::{Entity as EcsEntity, Join, WorldExt}; use vek::*; @@ -41,6 +42,12 @@ const SHADOW_MAX_DIST: f32 = 96.0; // The distance beyond which shadows may not /// Used for first person camera effects const RUNNING_THRESHOLD: f32 = 0.7; +struct EventLight { + light: Light, + timeout: f32, + fadeout: fn(f32) -> f32, +} + struct Skybox { model: Model, locals: Consts, @@ -57,6 +64,7 @@ pub struct Scene { shadows: Consts, camera: Camera, camera_input_state: Vec2, + event_lights: Vec, skybox: Skybox, postprocess: PostProcess, @@ -101,6 +109,7 @@ impl Scene { .unwrap(), camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson), camera_input_state: Vec2::zero(), + event_lights: Vec::new(), skybox: Skybox { model: renderer.create_model(&create_skybox_mesh()).unwrap(), @@ -135,9 +144,6 @@ impl Scene { /// Get a reference to the scene's particle manager. pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr } - /// Get a mutable reference to the scene's particle manager. - pub fn particle_mgr_mut(&mut self) -> &mut ParticleMgr { &mut self.particle_mgr } - /// Get a reference to the scene's figure manager. pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr } @@ -187,6 +193,23 @@ impl Scene { } } + pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { + match outcome { + Outcome::Explosion { pos, power, .. } => self.event_lights.push(EventLight { + light: Light::new( + *pos, + Rgb::new(1.0, 0.5, 0.0), + *power * 2.5, + ), + timeout: 0.5, + fadeout: |timeout| timeout * 2.0, + }), + _ => {}, + } + + self.particle_mgr.handle_outcome(&outcome, &scene_data); + } + /// Maintain data such as GPU constant buffers, models, etc. To be called /// once per tick. pub fn maintain( @@ -319,6 +342,9 @@ impl Scene { light_anim.strength, ) }) + .chain(self.event_lights + .iter() + .map(|el| el.light.with_strength((el.fadeout)(el.timeout)))) .collect::>(); lights.sort_by_key(|light| light.get_pos().distance_squared(player_pos) as i32); lights.truncate(MAX_LIGHT_COUNT); @@ -326,6 +352,13 @@ impl Scene { .update_consts(&mut self.lights, &lights) .expect("Failed to update light constants"); + // Update event lights + let dt = ecs.fetch::().0; + self.event_lights.drain_filter(|el| { + el.timeout -= dt; + el.timeout <= 0.0 + }); + // Update shadow constants let mut shadows = ( &scene_data.state.ecs().read_storage::(), diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 68aed3c937..ddc9595ff1 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -53,7 +53,13 @@ impl ParticleMgr { match outcome { Outcome::Explosion { pos, power } => { - for _ in 0..64 { + for _ in 0..150 { + self.particles.push(Particles { + alive_until: now + Duration::from_millis(250), + instance: ParticleInstance::new(time, rng.gen(), ParticleMode::Shrapnel, *pos), + }); + } + for _ in 0..200 { self.particles.push(Particles { alive_until: now + Duration::from_secs(4), instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power)), diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a6a417c9f3..15570db133 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1033,7 +1033,7 @@ impl PlayState for SessionState { .read_resource::>() .emit_now(sfx_event_item); } - self.scene.particle_mgr_mut().handle_outcome(&outcome, &scene_data); + self.scene.handle_outcome(&outcome, &scene_data); } } } From 023d46537027de0ef27bdc0564496b35e1c5092a Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 18:27:55 +0800 Subject: [PATCH 48/71] clear particle gpu instance buffer when disabled --- voxygen/src/scene/particle.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index ddc9595ff1..f3ff7e5c64 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -56,13 +56,23 @@ impl ParticleMgr { for _ in 0..150 { self.particles.push(Particles { alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::Shrapnel, *pos), + instance: ParticleInstance::new( + time, + rng.gen(), + ParticleMode::Shrapnel, + *pos, + ), }); } for _ in 0..200 { self.particles.push(Particles { alive_until: now + Duration::from_secs(4), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power)), + instance: ParticleInstance::new( + time, + rng.gen(), + ParticleMode::CampfireSmoke, + *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power), + ), }); } }, @@ -77,13 +87,15 @@ impl ParticleMgr { // remove dead particles self.particles.retain(|p| p.alive_until > now); + // add new particles self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); - - self.upload_particles(renderer); } else { + // remove all particles self.particles.clear(); } + + self.upload_particles(renderer); } fn upload_particles(&mut self, renderer: &mut Renderer) { From 267d5a141a836ed28639faeab311b41812a56c2a Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 18:59:38 +0800 Subject: [PATCH 49/71] cleanup unneeded matrix and shader code --- assets/voxygen/shaders/particle-vert.glsl | 32 +++++++---------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 49d2fd86ab..2f2914f63d 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -20,31 +20,15 @@ out float f_light; const float SCALE = 1.0 / 11.0; -// Φ = Golden Ratio -float PHI = 1.61803398874989484820459; - -float gold_noise(in vec2 xy, in float seed){ - return fract(tan(distance(xy * PHI, xy) * seed) * xy.x); -} - // Modes const int SMOKE = 0; const int FIRE = 1; const int GUN_POWDER_SPARK = 2; const int SHRAPNEL = 3; -// meters per second +// meters per second squared (acceleration) const float earth_gravity = 9.807; -mat4 translate(vec3 vec){ - return mat4( - vec4(1.0, 0.0, 0.0, 0.0), - vec4(0.0, 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(vec.x, vec.y, vec.z, 1.0) - ); -} - struct Attr { vec3 offs; float scale; @@ -66,8 +50,6 @@ float exp_scale(float factor) { } void main() { - mat4 inst_mat = translate(inst_pos); - float rand0 = hash(vec4(inst_entropy + 0)); float rand1 = hash(vec4(inst_entropy + 1)); float rand2 = hash(vec4(inst_entropy + 2)); @@ -126,14 +108,18 @@ void main() { ); } - f_pos = (inst_mat * vec4(v_pos * attr.scale * SCALE + attr.offs, 1)).xyz; + f_pos = inst_pos + (v_pos * attr.scale * SCALE + attr.offs); // First 3 normals are negative, next 3 are positive vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); - f_norm = (inst_mat * vec4(normals[(v_norm_ao >> 0) & 0x7u], 0)).xyz; + f_norm = + // inst_pos * + normals[(v_norm_ao >> 0) & 0x7u]; - vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; - f_col = srgb_to_linear(col) * srgb_to_linear(attr.col); + //vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; + f_col = + //srgb_to_linear(col) * + srgb_to_linear(attr.col); f_ao = float((v_norm_ao >> 3) & 0x3u) / 4.0; f_light = 1.0; From ba301696f4a3ec41fbbdf234988a25ab3dbedd6f Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 19:58:08 +0800 Subject: [PATCH 50/71] change campfire cmd to spawn object instead of npc --- server/src/cmd.rs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3d8d3bef72..70f6840834 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -7,7 +7,7 @@ use chrono::{NaiveTime, Timelike}; use common::{ assets, cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS}, - comp::{self, ChatType, Item}, + comp::{self, ChatType, Item, LightEmitter, WaypointArea}, event::{EventBus, ServerEvent}, msg::{Notification, PlayerListUpdate, ServerMsg}, npc::{self, get_npc_name}, @@ -674,24 +674,16 @@ fn handle_spawn_campfire( ) { match server.state.read_component_cloned::(target) { Some(pos) => { - let vel = Vec3::new( - rand::thread_rng().gen_range(-2.0, 3.0), - rand::thread_rng().gen_range(-2.0, 3.0), - 10.0, - ); - - let body = comp::Body::Object(comp::object::Body::CampfireLit); - - let mut stats = comp::Stats::new("Campfire".to_string(), body); - - // Level 0 will prevent exp gain from kill - stats.level.set_level(0); - server .state - .create_npc(pos, stats, comp::Loadout::default(), body) - .with(comp::Vel(vel)) - .with(comp::MountState::Unmounted) + .create_object(pos, comp::object::Body::CampfireLit) + .with(LightEmitter { + col: Rgb::new(1.0, 0.65, 0.2), + strength: 2.0, + flicker: 1.0, + animated: true, + }) + .with(WaypointArea::default()) .build(); server.notify_client( From bb8ba752873e959f57d141c331c6e6374b8c8b91 Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 20:03:01 +0800 Subject: [PATCH 51/71] cleanup redundant function --- common/src/comp/phys.rs | 2 +- common/src/util/dir.rs | 2 -- voxygen/src/audio/sfx/mod.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index b6fdb18d73..466734cb4c 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -25,7 +25,7 @@ impl Component for Vel { pub struct Ori(pub Dir); impl Ori { - pub fn vec(&self) -> &Vec3 { &self.0.vec() } + pub fn vec(&self) -> &Vec3 { &*self.0 } } impl Component for Ori { diff --git a/common/src/util/dir.rs b/common/src/util/dir.rs index 96c085479d..22aa1b0fe6 100644 --- a/common/src/util/dir.rs +++ b/common/src/util/dir.rs @@ -87,8 +87,6 @@ impl Dir { } pub fn is_valid(&self) -> bool { !self.0.map(f32::is_nan).reduce_or() && self.is_normalized() } - - pub fn vec(&self) -> &Vec3 { &self.0 } } impl std::ops::Deref for Dir { diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 6a4e0f8855..c823c0f648 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -204,7 +204,7 @@ impl<'a> TryFrom<&'a Outcome> for SfxEventItem { Outcome::ProjectileShot { pos, .. } => { Ok(Self::new(SfxEvent::ProjectileShot, Some(*pos), None)) }, - _ => Err(()), + // _ => Err(()), } } } From cc36e9c3000afd215907c4865d8dc16856f90de3 Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 20:17:50 +0800 Subject: [PATCH 52/71] fix rebase --- common/src/comp/inventory/item/tool.rs | 132 ++++++++++++------------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 8b09cc7324..b040f0517f 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -2,8 +2,7 @@ // version in voxygen\src\meta.rs in order to reset save files to being empty use crate::comp::{ - body::object, projectile, Body, CharacterAbility, Gravity, HealthChange, HealthSource, - LightEmitter, Projectile, + body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -194,7 +193,6 @@ impl Tool { recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_gravity: Some(Gravity(0.05)), }, ], Dagger(_) => vec![ @@ -263,62 +261,41 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), - projectile::Effect::RewardEnergy(150), - projectile::Effect::Vanish, - ], - time_left: Duration::from_secs(20), - owner: None, - }, - projectile_body: Body::Object(object::Body::BoltFire), - projectile_light: Some(LightEmitter { - col: (0.85, 0.5, 0.11).into(), - ..Default::default() - }), - projectile_gravity: None, - }, - BasicRanged { - energy_cost: 400, - holdable: true, - prepare_duration: Duration::from_millis(800), - recover_duration: Duration::from_millis(50), - projectile: Projectile { - hit_solid: vec![ - projectile::Effect::Explode { power: 1.4 }, - projectile::Effect::Vanish, - ], - hit_entity: vec![ - projectile::Effect::Explode { power: 1.4 }, - projectile::Effect::Vanish, - ], - time_left: Duration::from_secs(20), - owner: None, - }, - projectile_body: Body::Object(object::Body::BoltFireBig), - projectile_light: Some(LightEmitter { - col: (1.0, 0.75, 0.11).into(), - ..Default::default() - }), - projectile_gravity: None, - }, - ], - Staff(StaffKind::Sceptre) => vec![ - BasicMelee { - energy_cost: 0, - buildup_duration: Duration::from_millis(0), - recover_duration: Duration::from_millis(300), - base_healthchange: -1, - range: 10.0, - max_angle: 45.0, - }, - BasicMelee { - energy_cost: 350, - buildup_duration: Duration::from_millis(0), - recover_duration: Duration::from_millis(1000), - base_healthchange: 15, - range: 10.0, - max_angle: 45.0, - }, - ], + + projectile_gravity: None, + }, + BasicRanged { + energy_cost: 400, + holdable: true, + prepare_duration: Duration::from_millis(800), + recover_duration: Duration::from_millis(50), + projectile: Projectile { + hit_solid: vec![ + projectile::Effect::Explode { + power: 1.4 * self.base_power(), + }, + projectile::Effect::Vanish, + ], + hit_entity: vec![ + projectile::Effect::Explode { + power: 1.4 * self.base_power(), + }, + projectile::Effect::Vanish, + ], + time_left: Duration::from_secs(20), + owner: None, + }, + projectile_body: Body::Object(object::Body::BoltFireBig), + projectile_light: Some(LightEmitter { + col: (1.0, 0.75, 0.11).into(), + ..Default::default() + }), + + projectile_gravity: None, + }, + ] + } + }, Shield(_) => vec![ BasicMelee { energy_cost: 0, @@ -337,14 +314,35 @@ impl Tool { duration: Duration::from_millis(50), only_up: false, }, - projectile_body: Body::Object(object::Body::ArrowSnake), - projectile_light: Some(LightEmitter { - col: (0.0, 1.0, 0.33).into(), - ..Default::default() - }), - projectile_gravity: None, - }, - ], + CharacterAbility::Boost { + duration: Duration::from_millis(50), + only_up: true, + }, + BasicRanged { + energy_cost: 0, + holdable: false, + prepare_duration: Duration::from_millis(0), + recover_duration: Duration::from_millis(10), + projectile: Projectile { + hit_solid: vec![projectile::Effect::Stick], + hit_entity: vec![ + projectile::Effect::Stick, + projectile::Effect::Possess, + ], + time_left: Duration::from_secs(10), + owner: None, + }, + projectile_body: Body::Object(object::Body::ArrowSnake), + projectile_light: Some(LightEmitter { + col: (0.0, 1.0, 0.33).into(), + ..Default::default() + }), + projectile_gravity: None, + }, + ] + } else { + vec![] + } }, Empty => vec![BasicMelee { energy_cost: 0, From bf025df204a112687ceb0a493dcb6cc92e55ece6 Mon Sep 17 00:00:00 2001 From: scott-c Date: Tue, 4 Aug 2020 23:24:58 +0800 Subject: [PATCH 53/71] refactor sfx mgr outcome useage --- voxygen/src/audio/sfx/mod.rs | 51 +++++++++++++++++++++--------------- voxygen/src/scene/mod.rs | 38 ++++++++++++++++----------- voxygen/src/session.rs | 26 +++++++++--------- 3 files changed, 67 insertions(+), 48 deletions(-) diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index c823c0f648..143e2820c1 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -191,24 +191,6 @@ impl From<&InventoryUpdateEvent> for SfxEvent { } } -impl<'a> TryFrom<&'a Outcome> for SfxEventItem { - type Error = (); - - fn try_from(outcome: &'a Outcome) -> Result { - match outcome { - Outcome::Explosion { pos, power } => Ok(Self::new( - SfxEvent::Explosion, - Some(*pos), - Some((*power / 2.5).min(1.5)), - )), - Outcome::ProjectileShot { pos, .. } => { - Ok(Self::new(SfxEvent::ProjectileShot, Some(*pos), None)) - }, - // _ => Err(()), - } - } -} - #[derive(Deserialize)] pub struct SfxTriggerItem { pub files: Vec, @@ -251,13 +233,15 @@ impl SfxMgr { return; } - self.event_mapper - .maintain(state, player_entity, camera, &self.triggers); - let ecs = state.ecs(); audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir); + // deprecated in favor of outcomes + self.event_mapper + .maintain(state, player_entity, camera, &self.triggers); + + // deprecated in favor of outcomes let events = ecs.read_resource::>().recv_all(); for event in events { @@ -283,6 +267,31 @@ impl SfxMgr { } } + pub fn handle_outcome(&mut self, outcome: &Outcome, audio: &mut AudioFrontend) { + if !audio.sfx_enabled() { + return; + } + + match outcome { + Outcome::Explosion { pos, power } => { + audio.play_sfx( + // TODO: from sfx triggers config + "voxygen.audio.sfx.explosion", + *pos, + Some((*power / 2.5).min(1.5)), + ); + }, + Outcome::ProjectileShot { pos, body, .. } => { + audio.play_sfx( + // TODO: from sfx triggers config + "voxygen.audio.sfx.glider_open", + *pos, + None, + ); + }, + } + } + fn load_sfx_items() -> SfxTriggers { match assets::load_file("voxygen.audio.sfx", &["ron"]) { Ok(file) => match ron::de::from_reader(file) { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 5ee7f0257b..c635c02c83 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -21,10 +21,10 @@ use crate::{ use anim::character::SkeletonAttr; use common::{ comp, - state::{State, DeltaTime}, + outcome::Outcome, + state::{DeltaTime, State}, terrain::{BlockKind, TerrainChunk}, vol::ReadVol, - outcome::Outcome, }; use specs::{Entity as EcsEntity, Join, WorldExt}; use vek::*; @@ -193,21 +193,23 @@ impl Scene { } } - pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { + pub fn handle_outcome( + &mut self, + outcome: &Outcome, + scene_data: &SceneData, + audio: &mut AudioFrontend, + ) { + self.particle_mgr.handle_outcome(&outcome, &scene_data); + self.sfx_mgr.handle_outcome(&outcome, audio); + match outcome { Outcome::Explosion { pos, power, .. } => self.event_lights.push(EventLight { - light: Light::new( - *pos, - Rgb::new(1.0, 0.5, 0.0), - *power * 2.5, - ), + light: Light::new(*pos, Rgb::new(1.0, 0.5, 0.0), *power * 2.5), timeout: 0.5, fadeout: |timeout| timeout * 2.0, }), _ => {}, } - - self.particle_mgr.handle_outcome(&outcome, &scene_data); } /// Maintain data such as GPU constant buffers, models, etc. To be called @@ -342,9 +344,11 @@ impl Scene { light_anim.strength, ) }) - .chain(self.event_lights - .iter() - .map(|el| el.light.with_strength((el.fadeout)(el.timeout)))) + .chain( + self.event_lights + .iter() + .map(|el| el.light.with_strength((el.fadeout)(el.timeout))), + ) .collect::>(); lights.sort_by_key(|light| light.get_pos().distance_squared(player_pos) as i32); lights.truncate(MAX_LIGHT_COUNT); @@ -437,8 +441,12 @@ impl Scene { self.particle_mgr.maintain(renderer, &scene_data); // Maintain audio - self.sfx_mgr - .maintain(audio, scene_data.state, scene_data.player_entity, &self.camera); + self.sfx_mgr.maintain( + audio, + scene_data.state, + scene_data.player_entity, + &self.camera, + ); self.music_mgr.maintain(audio, scene_data.state); } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 15570db133..83bf35eb24 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -21,13 +21,13 @@ use common::{ }, event::EventBus, msg::ClientState, + outcome::Outcome, terrain::{Block, BlockKind}, util::Dir, vol::ReadVol, - outcome::Outcome, }; use specs::{Join, WorldExt}; -use std::{cell::RefCell, rc::Rc, time::Duration, convert::TryFrom}; +use std::{cell::RefCell, rc::Rc, time::Duration}; use tracing::{error, info}; use vek::*; @@ -101,7 +101,12 @@ impl SessionState { } /// Tick the session (and the client attached to it). - fn tick(&mut self, dt: Duration, global_state: &mut GlobalState, outcomes: &mut Vec) -> Result { + fn tick( + &mut self, + dt: Duration, + global_state: &mut GlobalState, + outcomes: &mut Vec, + ) -> Result { self.inputs.tick(dt); let mut client = self.client.borrow_mut(); @@ -638,7 +643,11 @@ impl PlayState for SessionState { // Runs if either in a multiplayer server or the singleplayer server is unpaused if !global_state.paused() { // Perform an in-game tick. - match self.tick(global_state.clock.get_avg_delta(), global_state, &mut outcomes) { + match self.tick( + global_state.clock.get_avg_delta(), + global_state, + &mut outcomes, + ) { Ok(TickAction::Continue) => {}, // Do nothing Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu Err(err) => { @@ -1026,14 +1035,7 @@ impl PlayState for SessionState { // Process outcomes from client for outcome in outcomes { - if let Ok(sfx_event_item) = SfxEventItem::try_from(&outcome) { - client - .state() - .ecs() - .read_resource::>() - .emit_now(sfx_event_item); - } - self.scene.handle_outcome(&outcome, &scene_data); + self.scene.handle_outcome(&outcome, &scene_data, &mut global_state.audio); } } } From a0107d5cda77a7f8aeab2dbced4e745965170ac0 Mon Sep 17 00:00:00 2001 From: scott-c Date: Fri, 7 Aug 2020 00:41:43 +0800 Subject: [PATCH 54/71] fix rebase --- client/src/lib.rs | 8 ++++---- common/src/outcome.rs | 10 +++++----- server/src/events/entity_creation.rs | 7 +++++-- server/src/sys/entity_sync.rs | 12 ++++++++---- voxygen/src/audio/channel.rs | 9 ++++++--- voxygen/src/audio/sfx/event_mapper/combat/mod.rs | 12 ++++++++---- voxygen/src/audio/sfx/event_mapper/mod.rs | 10 ++++++++-- voxygen/src/audio/sfx/event_mapper/movement/mod.rs | 12 ++++++++---- .../src/audio/sfx/event_mapper/progression/mod.rs | 8 +++++++- voxygen/src/audio/sfx/mod.rs | 6 +++--- voxygen/src/scene/mod.rs | 2 +- voxygen/src/scene/particle.rs | 2 +- voxygen/src/session.rs | 3 ++- 13 files changed, 66 insertions(+), 35 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 08109865db..f41d1efe04 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -25,12 +25,12 @@ use common::{ Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, }, + outcome::Outcome, recipe::RecipeBook, state::State, sync::{Uid, UidAllocator, WorldSyncExt}, terrain::{block::Block, TerrainChunk, TerrainChunkSize}, vol::RectVolSize, - outcome::Outcome, }; use futures_executor::block_on; use futures_timer::Delay; @@ -1231,9 +1231,9 @@ impl Client { self.view_distance = Some(vd); frontend_events.push(Event::SetViewDistance(vd)); }, - ServerMsg::Outcomes(outcomes) => frontend_events.extend(outcomes - .into_iter() - .map(Event::Outcome)), + ServerMsg::Outcomes(outcomes) => { + frontend_events.extend(outcomes.into_iter().map(Event::Outcome)) + }, } } } diff --git a/common/src/outcome.rs b/common/src/outcome.rs index 9f047bcc26..4f89f418b0 100644 --- a/common/src/outcome.rs +++ b/common/src/outcome.rs @@ -2,10 +2,11 @@ use crate::comp; use serde::{Deserialize, Serialize}; use vek::*; -/// An outcome represents the final result of an instantaneous event. It implies that said event has -/// already occurred. It is not a request for that event to occur, nor is it something that may be -/// cancelled or otherwise altered. Its primary purpose is to act as something for frontends (both -/// server and client) to listen to in order to receive feedback about events in the world. +/// An outcome represents the final result of an instantaneous event. It implies +/// that said event has already occurred. It is not a request for that event to +/// occur, nor is it something that may be cancelled or otherwise altered. Its +/// primary purpose is to act as something for frontends (both server and +/// client) to listen to in order to receive feedback about events in the world. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Outcome { Explosion { @@ -19,7 +20,6 @@ pub enum Outcome { }, } - impl Outcome { pub fn get_pos(&self) -> Option> { match self { diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 0d9a8ff61a..f894268737 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -4,8 +4,8 @@ use common::{ self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, - util::Dir, outcome::Outcome, + util::Dir, }; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -93,7 +93,10 @@ pub fn handle_shoot( let vel = *dir * 100.0; // Add an outcome - state.ecs().write_resource::>().push(Outcome::ProjectileShot { pos, body, vel }); + state + .ecs() + .write_resource::>() + .push(Outcome::ProjectileShot { pos, body, vel }); // TODO: Player height pos.z += 1.2; diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index 6e4a5fa897..2dc20648b4 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -7,14 +7,14 @@ use crate::{ Tick, }; use common::{ - comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel, Player}, + comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel}, msg::ServerMsg, outcome::Outcome, region::{Event as RegionEvent, RegionMap}, state::TimeOfDay, sync::{CompSyncPackage, Uid}, - vol::RectVolSize, terrain::TerrainChunkSize, + vol::RectVolSize, }; use specs::{ Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage, @@ -326,8 +326,12 @@ impl<'a> System<'a> for Sys { // Sync outcomes for (client, player, pos) in (&mut clients, &players, positions.maybe()).join() { - let is_near = |o_pos: Vec3| pos - .zip_with(player.view_distance, |pos, vd| pos.0.xy().distance_squared(o_pos.xy()) < (vd as f32 * TerrainChunkSize::RECT_SIZE.x as f32).powf(2.0)); + let is_near = |o_pos: Vec3| { + pos.zip_with(player.view_distance, |pos, vd| { + pos.0.xy().distance_squared(o_pos.xy()) + < (vd as f32 * TerrainChunkSize::RECT_SIZE.x as f32).powf(2.0) + }) + }; let outcomes = outcomes .iter() diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 6662e0efd0..5f19e42532 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -171,8 +171,11 @@ impl SfxChannel { pub fn update(&mut self, listener: &Listener) { const FALLOFF: f32 = 0.13; - self.sink.set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array()); - self.sink.set_left_ear_position(listener.ear_left_rpos.into_array()); - self.sink.set_right_ear_position(listener.ear_right_rpos.into_array()); + self.sink + .set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array()); + self.sink + .set_left_ear_position(listener.ear_left_rpos.into_array()); + self.sink + .set_right_ear_position(listener.ear_right_rpos.into_array()); } } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs index 5a81800930..56aca955b0 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs @@ -41,7 +41,13 @@ pub struct CombatEventMapper { } impl EventMapper for CombatEventMapper { - fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); @@ -56,9 +62,7 @@ impl EventMapper for CombatEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR - }) + .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR) { if let Some(character) = character { let state = self.event_history.entry(entity).or_default(); diff --git a/voxygen/src/audio/sfx/event_mapper/mod.rs b/voxygen/src/audio/sfx/event_mapper/mod.rs index 4ac331fa86..d5a9d6f8cb 100644 --- a/voxygen/src/audio/sfx/event_mapper/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/mod.rs @@ -8,11 +8,17 @@ use combat::CombatEventMapper; use movement::MovementEventMapper; use progression::ProgressionEventMapper; -use crate::scene::Camera; use super::SfxTriggers; +use crate::scene::Camera; trait EventMapper { - fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers); + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ); } pub struct SfxEventMapper { diff --git a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs index da6d29edc7..55bff58c96 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs @@ -38,7 +38,13 @@ pub struct MovementEventMapper { } impl EventMapper for MovementEventMapper { - fn maintain(&mut self, state: &State, player_entity: specs::Entity, camera: &Camera, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); @@ -55,9 +61,7 @@ impl EventMapper for MovementEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR - }) + .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR) { if let Some(character) = character { let state = self.event_history.entry(entity).or_default(); diff --git a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs index 64c21b5303..499e39e559 100644 --- a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs @@ -26,7 +26,13 @@ pub struct ProgressionEventMapper { impl EventMapper for ProgressionEventMapper { #[allow(clippy::op_ref)] // TODO: Pending review in #587 - fn maintain(&mut self, state: &State, player_entity: specs::Entity, _camera: &Camera, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + _camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let next_state = ecs.read_storage::().get(player_entity).map_or( diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 143e2820c1..d85d7b2765 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -88,8 +88,8 @@ use crate::{audio::AudioFrontend, scene::Camera}; use common::{ assets, comp::{ - item::{Consumable, ItemKind, ToolCategory}, - CharacterAbilityType, InventoryUpdateEvent, Ori, Pos, + item::{ItemKind, ToolCategory}, + CharacterAbilityType, InventoryUpdateEvent, }, event::EventBus, outcome::Outcome, @@ -281,7 +281,7 @@ impl SfxMgr { Some((*power / 2.5).min(1.5)), ); }, - Outcome::ProjectileShot { pos, body, .. } => { + Outcome::ProjectileShot { pos, .. } => { audio.play_sfx( // TODO: from sfx triggers config "voxygen.audio.sfx.glider_open", diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index c635c02c83..5eacf0870b 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -208,7 +208,7 @@ impl Scene { timeout: 0.5, fadeout: |timeout| timeout * 2.0, }), - _ => {}, + Outcome::ProjectileShot { .. } => {}, } } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index f3ff7e5c64..6784390b5a 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -76,7 +76,7 @@ impl ParticleMgr { }); } }, - _ => {}, + Outcome::ProjectileShot { .. } => {}, } } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 83bf35eb24..b852157a96 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1035,7 +1035,8 @@ impl PlayState for SessionState { // Process outcomes from client for outcome in outcomes { - self.scene.handle_outcome(&outcome, &scene_data, &mut global_state.audio); + self.scene + .handle_outcome(&outcome, &scene_data, &mut global_state.audio); } } } From dd1e89a691546c7636b20302721a305fe19752e5 Mon Sep 17 00:00:00 2001 From: scott-c Date: Fri, 7 Aug 2020 22:04:52 +0800 Subject: [PATCH 55/71] Implement particle heartbeat scheduler --- voxygen/src/render/pipelines/particle.rs | 12 +- voxygen/src/scene/particle.rs | 318 +++++++++++++---------- 2 files changed, 190 insertions(+), 140 deletions(-) diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index e8e222e224..eae013242d 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -94,15 +94,11 @@ impl ParticleMode { } impl Instance { - pub fn new( - inst_time: f64, - inst_entropy: f32, - inst_mode: ParticleMode, - inst_pos: Vec3, - ) -> Self { + pub fn new(inst_time: f64, inst_mode: ParticleMode, inst_pos: Vec3) -> Self { + use rand::Rng; Self { inst_time: inst_time as f32, - inst_entropy, + inst_entropy: rand::thread_rng().gen(), inst_mode: inst_mode as i32, inst_pos: inst_pos.into_array(), } @@ -110,7 +106,7 @@ impl Instance { } impl Default for Instance { - fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } + fn default() -> Self { Self::new(0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 6784390b5a..11e3b74a1d 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -19,61 +19,51 @@ use specs::{Join, WorldExt}; use std::time::{Duration, Instant}; use vek::*; -struct Particles { - alive_until: Instant, // created_at + lifespan - instance: ParticleInstance, -} - pub struct ParticleMgr { - // keep track of lifespans - particles: Vec, + /// keep track of lifespans + particles: Vec, + + /// keep track of timings + scheduler: HeartbeatScheduler, + + /// GPU Instance Buffer instances: Instances, + + /// GPU Vertex Buffers model_cache: HashMap<&'static str, Model>, } -const MODEL_KEY: &str = "voxygen.voxel.particle"; - impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { Self { particles: Vec::new(), + scheduler: HeartbeatScheduler::new(), instances: default_instances(renderer), model_cache: default_cache(renderer), } } - pub fn particle_count(&self) -> usize { self.instances.count() } - - pub fn particle_count_visible(&self) -> usize { self.instances.count() } - pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { let time = scene_data.state.get_time(); - let now = Instant::now(); let mut rng = rand::thread_rng(); match outcome { Outcome::Explosion { pos, power } => { for _ in 0..150 { - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new( - time, - rng.gen(), - ParticleMode::Shrapnel, - *pos, - ), - }); + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::Shrapnel, + *pos, + )); } for _ in 0..200 { - self.particles.push(Particles { - alive_until: now + Duration::from_secs(4), - instance: ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power), - ), - }); + self.particles.push(Particle::new( + Duration::from_secs(4), + time, + ParticleMode::CampfireSmoke, + *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power), + )); } }, Outcome::ProjectileShot { .. } => {}, @@ -84,35 +74,26 @@ impl ParticleMgr { if scene_data.particles_enabled { let now = Instant::now(); - // remove dead particles + // remove dead Particle self.particles.retain(|p| p.alive_until > now); - // add new particles + // add new Particle self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); + + // update timings + self.scheduler.maintain(); } else { - // remove all particles + // remove all particle lifespans self.particles.clear(); + + // remove all timings + self.scheduler.clear(); } self.upload_particles(renderer); } - fn upload_particles(&mut self, renderer: &mut Renderer) { - let all_cpu_instances = self - .particles - .iter() - .map(|p| p.instance) - .collect::>(); - - // TODO: optimise buffer writes - let gpu_instances = renderer - .create_instances(&all_cpu_instances) - .expect("Failed to upload particle instances to the GPU!"); - - self.instances = gpu_instances; - } - fn maintain_body_particles(&mut self, scene_data: &SceneData) { let ecs = scene_data.state.ecs(); for (_i, (_entity, body, pos)) in ( @@ -141,106 +122,93 @@ impl ParticleMgr { fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(10), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + self.particles.push(Particle::new( + Duration::from_secs(10), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); - - self.particles.push(Particles { - alive_until: now + Duration::from_secs(1), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); + self.particles.push(Particle::new( + Duration::from_secs(1), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boltfirebig_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); // fire - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(3)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); + } // smoke - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) { + self.particles.push(Particle::new( + Duration::from_secs(2), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_bomb_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - // sparks - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + // sparks + self.particles.push(Particle::new( + Duration::from_millis(1500), + time, + ParticleMode::GunPowderSpark, + pos.0, + )); - // smoke - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + // smoke + self.particles.push(Particle::new( + Duration::from_secs(2), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boost_particles(&mut self, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); let time = state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); for (_i, (_entity, pos, character_state)) in ( &ecs.entities(), @@ -251,19 +219,33 @@ impl ParticleMgr { .enumerate() { if let CharacterState::Boost(_) = character_state { - self.particles.push(Particles { - alive_until: now + Duration::from_secs(15), - instance: ParticleInstance::new( + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_secs(15), time, - rng.gen(), ParticleMode::CampfireSmoke, pos.0, - ), - }); + )); + } } } } + fn upload_particles(&mut self, renderer: &mut Renderer) { + let all_cpu_instances = self + .particles + .iter() + .map(|p| p.instance) + .collect::>(); + + // TODO: optimise buffer writes + let gpu_instances = renderer + .create_instances(&all_cpu_instances) + .expect("Failed to upload particle instances to the GPU!"); + + self.instances = gpu_instances; + } + pub fn render( &self, renderer: &mut Renderer, @@ -275,12 +257,16 @@ impl ParticleMgr { if scene_data.particles_enabled { let model = &self .model_cache - .get(MODEL_KEY) + .get(DEFAULT_MODEL_KEY) .expect("Expected particle model in cache"); renderer.render_particles(model, globals, &self.instances, lights, shadows); } } + + pub fn particle_count(&self) -> usize { self.instances.count() } + + pub fn particle_count_visible(&self) -> usize { self.instances.count() } } fn default_instances(renderer: &mut Renderer) -> Instances { @@ -291,14 +277,16 @@ fn default_instances(renderer: &mut Renderer) -> Instances { .expect("Failed to upload particle instances to the GPU!") } +const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle"; + fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model> { let mut model_cache = HashMap::new(); - model_cache.entry(MODEL_KEY).or_insert_with(|| { + model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| { let offset = Vec3::zero(); let lod_scale = Vec3::one(); - let vox = assets::load_expect::(MODEL_KEY); + let vox = assets::load_expect::(DEFAULT_MODEL_KEY); let mesh = &Meshable::::generate_mesh( &Segment::from(vox.as_ref()), @@ -313,3 +301,69 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model, +} + +impl HeartbeatScheduler { + pub fn new() -> Self { + HeartbeatScheduler { + timers: HashMap::new(), + } + } + + /// updates the last elapsed times and elasped counts + /// this should be called once, and only once per tick. + pub fn maintain(&mut self) { + for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() { + // the number of iterations since last update + *heartbeats = + // TODO: use nightly api once stable; https://github.com/rust-lang/rust/issues/63139 + (last_update.elapsed().as_secs_f32() / frequency.as_secs_f32()).floor() as u8; + + // Instant::now() minus the heart beat count precision, + // or alternatively as expressed below. + *last_update += frequency.mul_f32(*heartbeats as f32); + // Note: we want to preserve incomplete heartbeats, and include them + // in the next update. + } + } + + /// returns the number of times this duration has elasped since the last + /// tick: + /// - if it's more frequent then tick rate, it could be 1 or more. + /// - if it's less frequent then tick rate, it could be 1 or 0. + /// - if it's equal to the tick rate, it could be between 2 and 0, due to + /// delta time variance. + pub fn heartbeats(&mut self, frequency: Duration) -> u8 { + self.timers + .entry(frequency) + .or_insert_with(|| (Instant::now(), 0)) + .1 + } + + pub fn clear(&mut self) { self.timers.clear() } +} + +struct Particle { + alive_until: Instant, // created_at + lifespan + instance: ParticleInstance, +} + +impl Particle { + fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3) -> Self { + Particle { + alive_until: Instant::now() + lifespan, + instance: ParticleInstance::new(time, mode, pos), + } + } +} From 0ace0acdcdd65f8dcce914f824ac04c1df143a23 Mon Sep 17 00:00:00 2001 From: scott-c Date: Fri, 7 Aug 2020 22:22:17 +0800 Subject: [PATCH 56/71] update comment --- voxygen/src/audio/sfx/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index d85d7b2765..783056271b 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -237,11 +237,11 @@ impl SfxMgr { audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir); - // deprecated in favor of outcomes + // TODO: replace; deprecated in favor of outcomes self.event_mapper .maintain(state, player_entity, camera, &self.triggers); - // deprecated in favor of outcomes + // TODO: replace; deprecated in favor of outcomes let events = ecs.read_resource::>().recv_all(); for event in events { From 70ff8c3faa287ea44b00195654406cec4b3b961e Mon Sep 17 00:00:00 2001 From: scott-c Date: Sat, 8 Aug 2020 19:18:59 +0800 Subject: [PATCH 57/71] Add Copy trait to dependents --- voxygen/src/scene/camera.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 65aaa7bbe2..b074f73ef5 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -24,7 +24,7 @@ impl Default for CameraMode { fn default() -> Self { Self::ThirdPerson } } -#[derive(Clone)] +#[derive(Clone, Copy)] pub struct Dependents { pub view_mat: Mat4, pub proj_mat: Mat4, @@ -116,7 +116,7 @@ impl Camera { ) } - pub fn dependents(&self) -> Dependents { self.dependents.clone() } + pub fn dependents(&self) -> Dependents { &self.dependents } /// Rotate the camera about its focus by the given delta, limiting the input /// accordingly. From cae84dd4c9c61afb85ef004e9771b7bd9b575ac0 Mon Sep 17 00:00:00 2001 From: scott-c Date: Sat, 8 Aug 2020 20:53:07 +0800 Subject: [PATCH 58/71] fix rebase --- server/src/cmd.rs | 2 +- server/src/events/entity_creation.rs | 1 + server/src/events/entity_manipulation.rs | 21 +++++++++++++++++---- server/src/sys/sentinel.rs | 2 +- voxygen/src/scene/camera.rs | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 70f6840834..5a282766fd 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -672,7 +672,7 @@ fn handle_spawn_campfire( _args: String, _action: &ChatCommand, ) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { server .state diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index f894268737..5e910452c9 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -7,6 +7,7 @@ use common::{ outcome::Outcome, util::Dir, }; +use comp::group; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d87e61ea08..ff6f4ae2b6 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -278,16 +278,29 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { } } -pub fn handle_explosion(server: &Server, pos: Vec3, power: f32, owner: Option) { +pub fn handle_explosion( + server: &Server, + pos: Vec3, + power: f32, + owner: Option, + friendly_damage: bool, +) { + // Go through all other entities + let hit_range = 3.0 * power; let ecs = &server.state.ecs(); // Add an outcome ecs.write_resource::>() .push(Outcome::Explosion { pos, power }); - // Go through all other entities - let hit_range = 3.0 * power; - for (pos_b, ori_b, character_b, stats_b, loadout_b) in ( + let owner_entity = owner.and_then(|uid| { + ecs.read_resource::() + .retrieve_entity_internal(uid.into()) + }); + let groups = ecs.read_storage::(); + + for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in ( + &ecs.entities(), &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 8bda2339e8..adcc76bd30 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,7 +1,7 @@ use super::SysTimer; use common::{ comp::{ - Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, + Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, }, msg::EcsCompPacket, diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index b074f73ef5..deaadcc963 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -116,7 +116,7 @@ impl Camera { ) } - pub fn dependents(&self) -> Dependents { &self.dependents } + pub fn dependents(&self) -> Dependents { self.dependents } /// Rotate the camera about its focus by the given delta, limiting the input /// accordingly. From 691607f3987f106b00caebd9c0a49684f24bc7bd Mon Sep 17 00:00:00 2001 From: jshipsey Date: Sat, 1 Aug 2020 22:59:22 -0400 Subject: [PATCH 59/71] swimming changes --- voxygen/src/anim/Cargo.toml | 2 +- voxygen/src/anim/src/character/climb.rs | 489 ++++++++++++++++++------ voxygen/src/anim/src/character/run.rs | 12 +- voxygen/src/anim/src/character/swim.rs | 85 ++-- voxygen/src/scene/figure/mod.rs | 1 + voxygen/src/session.rs | 7 +- voxygen/src/settings.rs | 12 +- voxygen/src/window.rs | 11 +- 8 files changed, 451 insertions(+), 168 deletions(-) diff --git a/voxygen/src/anim/Cargo.toml b/voxygen/src/anim/Cargo.toml index ab4d0e2bfe..92ce71acb0 100644 --- a/voxygen/src/anim/Cargo.toml +++ b/voxygen/src/anim/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" name = "voxygen_anim" # Uncomment to use animation hot reloading # Note: this breaks `cargo test` -# crate-type = ["lib", "cdylib"] +crate-type = ["lib", "cdylib"] [features] use-dyn-lib = ["libloading", "notify", "lazy_static", "tracing", "find_folder"] diff --git a/voxygen/src/anim/src/character/climb.rs b/voxygen/src/anim/src/character/climb.rs index 47257ed641..adcb48b5c4 100644 --- a/voxygen/src/anim/src/character/climb.rs +++ b/voxygen/src/anim/src/character/climb.rs @@ -1,6 +1,7 @@ use super::{super::Animation, CharacterSkeleton, SkeletonAttr}; use common::comp::item::{Hands, ToolKind}; -use std::f32::consts::PI; +use std::{f32::consts::PI, ops::Mul}; + use vek::*; pub struct ClimbAnimation; @@ -21,19 +22,20 @@ impl Animation for ClimbAnimation { #[cfg_attr(feature = "be-dyn-lib", export_name = "character_climb")] fn update_skeleton_inner( skeleton: &Self::Skeleton, - (active_tool_kind, second_tool_kind, velocity, _orientation, _global_time): Self::Dependency, + (active_tool_kind, second_tool_kind, velocity, _orientation, global_time): Self::Dependency, anim_time: f64, rate: &mut f32, skeleton_attr: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let speed = velocity.magnitude(); + let speed = velocity.z; *rate = speed; - let constant = 1.0; let smooth = (anim_time as f32 * constant as f32 * 1.5).sin(); let smootha = (anim_time as f32 * constant as f32 * 1.5 + PI / 2.0).sin(); + let drop = (anim_time as f32 * constant as f32 * 4.0 + PI / 2.0).sin(); + let dropa = (anim_time as f32 * constant as f32 * 4.0).sin(); let quick = (((5.0) / (0.6 + 4.0 * ((anim_time as f32 * constant as f32 * 1.5).sin()).powf(2.0 as f32))) @@ -46,145 +48,386 @@ impl Animation for ClimbAnimation { .powf(2.0 as f32))) .sqrt()) * ((anim_time as f32 * constant as f32 * 1.5 + PI / 2.0).sin()); - - next.head.offset = Vec3::new( - 0.0, - -4.0 + skeleton_attr.head.0, - skeleton_attr.head.1 + smootha * 0.2, + let head_look = Vec2::new( + ((global_time + anim_time) as f32 / 2.0) + .floor() + .mul(7331.0) + .sin() + * 0.3, + ((global_time + anim_time) as f32 / 2.0) + .floor() + .mul(1337.0) + .sin() + * 0.15, ); - next.head.ori = Quaternion::rotation_z(smooth * 0.1) - * Quaternion::rotation_x(0.6) - * Quaternion::rotation_y(quick * 0.1); - next.head.scale = Vec3::one() * skeleton_attr.head_scale; + if speed > 0.7 { + next.head.offset = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + skeleton_attr.head.1 + smootha * 0.2, + ); + next.head.ori = Quaternion::rotation_z(smooth * 0.1) + * Quaternion::rotation_x(0.6) + * Quaternion::rotation_y(quick * 0.1); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; - next.chest.offset = Vec3::new( - 0.0, - skeleton_attr.chest.0 + 1.0, - skeleton_attr.chest.1 + smootha * 1.1, - ); - next.chest.ori = Quaternion::rotation_z(quick * 0.25) - * Quaternion::rotation_x(-0.15) - * Quaternion::rotation_y(quick * -0.12); - next.chest.scale = Vec3::one(); + next.chest.offset = Vec3::new( + 0.0, + skeleton_attr.chest.0, + skeleton_attr.chest.1 + smootha * 1.1, + ); + next.chest.ori = Quaternion::rotation_z(quick * 0.25) + * Quaternion::rotation_x(-0.15) + * Quaternion::rotation_y(quick * -0.12); + next.chest.scale = Vec3::one(); - next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 1.0, skeleton_attr.belt.1); - next.belt.ori = Quaternion::rotation_z(quick * 0.0) * Quaternion::rotation_x(0.0); - next.belt.scale = Vec3::one(); + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 1.0, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_z(quick * 0.0) * Quaternion::rotation_x(0.0); + next.belt.scale = Vec3::one(); - next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.ori = Quaternion::rotation_x(-0.2); - next.back.scale = Vec3::one() * 1.02; + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x(-0.2); + next.back.scale = Vec3::one() * 1.02; - next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_z(quick * 0.0) - * Quaternion::rotation_x(0.1) - * Quaternion::rotation_y(quick * 0.10); - next.shorts.scale = Vec3::one(); + next.shorts.offset = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_z(quick * 0.0) + * Quaternion::rotation_x(0.1) + * Quaternion::rotation_y(quick * 0.10); + next.shorts.scale = Vec3::one(); - next.l_hand.offset = Vec3::new( - -skeleton_attr.hand.0, - skeleton_attr.hand.1 + quicka * 1.5, - 5.0 + skeleton_attr.hand.2 - quick * 4.0, - ); - next.l_hand.ori = Quaternion::rotation_x(2.2 + quicka * 0.5); - next.l_hand.scale = Vec3::one(); + next.l_hand.offset = Vec3::new( + -skeleton_attr.hand.0, + 4.0 + skeleton_attr.hand.1 + quicka * 1.5, + 5.0 + skeleton_attr.hand.2 - quick * 4.0, + ); + next.l_hand.ori = Quaternion::rotation_x(2.2 + quicka * 0.5); + next.l_hand.scale = Vec3::one(); - next.r_hand.offset = Vec3::new( - skeleton_attr.hand.0, - skeleton_attr.hand.1 - quicka * 1.5, - 5.0 + skeleton_attr.hand.2 + quick * 4.0, - ); - next.r_hand.ori = Quaternion::rotation_x(2.2 - quicka * 0.5); - next.r_hand.scale = Vec3::one(); + next.r_hand.offset = Vec3::new( + skeleton_attr.hand.0, + 5.0 + skeleton_attr.hand.1 - quicka * 1.5, + 5.0 + skeleton_attr.hand.2 + quick * 4.0, + ); + next.r_hand.ori = Quaternion::rotation_x(2.2 - quicka * 0.5); + next.r_hand.scale = Vec3::one(); - next.l_foot.offset = Vec3::new( - -skeleton_attr.foot.0, - 1.0 + skeleton_attr.foot.1, - skeleton_attr.foot.2 + quick * 2.5, - ); - next.l_foot.ori = Quaternion::rotation_x(0.2 - quicka * 0.5); - next.l_foot.scale = Vec3::one(); + match active_tool_kind { + Some(ToolKind::Dagger(_)) => { + next.main.offset = Vec3::new(-4.0, -5.0, 7.0); + next.main.ori = + Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(1.5 * PI); + }, + Some(ToolKind::Shield(_)) => { + next.main.offset = Vec3::new(-0.0, -5.0, 3.0); + next.main.ori = + Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); + }, + _ => { + next.main.offset = Vec3::new(-7.0, -5.0, 15.0); + next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + }, + } + next.main.scale = Vec3::one(); - next.r_foot.offset = Vec3::new( - skeleton_attr.foot.0, - 1.0 + skeleton_attr.foot.1, - skeleton_attr.foot.2 - quick * 2.5, - ); - next.r_foot.ori = Quaternion::rotation_x(0.2 + quicka * 0.5); - next.r_foot.scale = Vec3::one(); + match second_tool_kind { + Some(ToolKind::Dagger(_)) => { + next.second.offset = Vec3::new(4.0, -6.0, 7.0); + next.second.ori = + Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); + }, + Some(ToolKind::Shield(_)) => { + next.second.offset = Vec3::new(0.0, -4.0, 3.0); + next.second.ori = + Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(1.5 * PI); + }, + _ => { + next.second.offset = Vec3::new(-7.0, -5.0, 15.0); + next.second.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + }, + } + next.second.scale = Vec3::one(); + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + 5.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2 + quick * 2.5, + ); + next.l_foot.ori = Quaternion::rotation_x(0.2 - quicka * 0.5); + next.l_foot.scale = Vec3::one(); - next.l_shoulder.offset = Vec3::new( - -skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.l_shoulder.ori = Quaternion::rotation_x(smootha * 0.15); - next.l_shoulder.scale = Vec3::one() * 1.1; + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 4.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2 - quick * 2.5, + ); + next.r_foot.ori = Quaternion::rotation_x(0.2 + quicka * 0.5); + next.r_foot.scale = Vec3::one(); - next.r_shoulder.offset = Vec3::new( - skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.r_shoulder.ori = Quaternion::rotation_x(smooth * 0.15); - next.r_shoulder.scale = Vec3::one() * 1.1; + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.ori = Quaternion::rotation_x(smootha * 0.15); + next.l_shoulder.scale = Vec3::one() * 1.1; - next.glider.offset = Vec3::new(0.0, 0.0, 10.0); - next.glider.scale = Vec3::one() * 0.0; + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.ori = Quaternion::rotation_x(smooth * 0.15); + next.r_shoulder.scale = Vec3::one() * 1.1; - match active_tool_kind { - Some(ToolKind::Dagger(_)) => { - next.main.offset = Vec3::new(-4.0, -5.0, 7.0); - next.main.ori = - Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(1.5 * PI); - }, - Some(ToolKind::Shield(_)) => { - next.main.offset = Vec3::new(-0.0, -5.0, 3.0); - next.main.ori = - Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); - }, - _ => { - next.main.offset = Vec3::new(-7.0, -5.0, 15.0); + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.main.offset = Vec3::new(-7.0, -5.0, 18.0); + next.main.ori = + Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57 + smootha * 0.25); + next.main.scale = Vec3::one(); + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = + Quaternion::rotation_x(smooth * -0.3) * Quaternion::rotation_y(smooth * -0.3); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.offset = Vec3::new(0.0, -0.2 + smooth * -0.08, 0.4) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.control.scale = Vec3::one(); + + next.l_control.scale = Vec3::one(); + + next.r_control.scale = Vec3::one(); + } else { + if speed > -0.7 { + next.head.offset = + Vec3::new(0.0, -2.0 + skeleton_attr.head.0, skeleton_attr.head.1); + next.head.ori = Quaternion::rotation_x(2.0 * head_look.y.abs()) + * Quaternion::rotation_z(3.5 * head_look.x.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + + next.chest.offset = Vec3::new( + 0.0, + -2.0 + skeleton_attr.chest.0 + 3.0, + skeleton_attr.chest.1, + ); + next.chest.ori = Quaternion::rotation_z(0.0) + * Quaternion::rotation_x(0.3) + * Quaternion::rotation_z(0.6); + next.chest.scale = Vec3::one(); + + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.1); + next.belt.scale = Vec3::one(); + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x(-0.2); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.offset = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_z(0.0) + * Quaternion::rotation_x(0.1) + * Quaternion::rotation_y(0.0); + next.shorts.scale = Vec3::one(); + + next.l_hand.offset = Vec3::new( + -skeleton_attr.hand.0, + 2.0 + skeleton_attr.hand.1, + skeleton_attr.hand.2, + ); + next.l_hand.ori = Quaternion::rotation_x(0.8); + next.l_hand.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new( + skeleton_attr.hand.0, + 5.5 + skeleton_attr.hand.1, + 5.0 + skeleton_attr.hand.2, + ); + next.r_hand.ori = Quaternion::rotation_x(2.2) * Quaternion::rotation_y(-0.5); + next.r_hand.scale = Vec3::one(); + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + 5.0 + skeleton_attr.foot.1, + 1.0 + skeleton_attr.foot.2, + ); + next.l_foot.ori = Quaternion::rotation_x(0.55); + next.l_foot.scale = Vec3::one(); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 5.0 + skeleton_attr.foot.1, + -2.0 + skeleton_attr.foot.2, + ); + next.r_foot.ori = Quaternion::rotation_x(0.2); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.ori = Quaternion::rotation_x(0.0); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.ori = Quaternion::rotation_x(0.0); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.main.offset = Vec3::new(-7.0, -5.0, 18.0); next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - }, - } - next.main.scale = Vec3::one(); + next.main.scale = Vec3::one(); - match second_tool_kind { - Some(ToolKind::Dagger(_)) => { - next.second.offset = Vec3::new(4.0, -6.0, 7.0); - next.second.ori = - Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); - }, - Some(ToolKind::Shield(_)) => { - next.second.offset = Vec3::new(0.0, -4.0, 3.0); - next.second.ori = - Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(1.5 * PI); - }, - _ => { - next.second.offset = Vec3::new(-7.0, -5.0, 15.0); - next.second.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - }, - } - next.second.scale = Vec3::one(); + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; - next.lantern.offset = Vec3::new( - skeleton_attr.lantern.0, - skeleton_attr.lantern.1, - skeleton_attr.lantern.2, - ); - next.lantern.ori = - Quaternion::rotation_x(smooth * -0.3) * Quaternion::rotation_y(smooth * -0.3); - next.lantern.scale = Vec3::one() * 0.65; + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.lantern.scale = Vec3::one() * 0.65; - next.torso.offset = Vec3::new(0.0, -0.2 + smooth * -0.08, 0.4) * skeleton_attr.scaler; - next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + next.torso.offset = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; - next.control.scale = Vec3::one(); + next.control.scale = Vec3::one(); - next.l_control.scale = Vec3::one(); + next.l_control.scale = Vec3::one(); + next.r_control.scale = Vec3::one(); + } else { + next.head.offset = + Vec3::new(0.0, -1.0 + skeleton_attr.head.0, skeleton_attr.head.1); + next.head.ori = Quaternion::rotation_x(-0.25) * Quaternion::rotation_z(0.0); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + + next.chest.offset = Vec3::new( + 0.0, + -2.0 + skeleton_attr.chest.0 + 3.0, + skeleton_attr.chest.1, + ); + next.chest.ori = Quaternion::rotation_z(0.0) + * Quaternion::rotation_x(0.2 + drop * 0.05) + * Quaternion::rotation_z(0.0); + next.chest.scale = Vec3::one(); + + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); + next.belt.ori = + Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.1 + dropa * 0.1); + next.belt.scale = Vec3::one(); + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x(-0.15); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.offset = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_z(0.0) + * Quaternion::rotation_x(0.1 + dropa * 0.12) + * Quaternion::rotation_y(0.0); + next.shorts.scale = Vec3::one(); + + next.l_hand.offset = Vec3::new( + -skeleton_attr.hand.0, + 7.5 + skeleton_attr.hand.1, + 7.0 + skeleton_attr.hand.2 + dropa * -1.0, + ); + next.l_hand.ori = + Quaternion::rotation_x(2.2) * Quaternion::rotation_y(0.3 + dropa * 0.1); + next.l_hand.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new( + skeleton_attr.hand.0, + 7.5 + skeleton_attr.hand.1, + 5.0 + skeleton_attr.hand.2 + drop * -1.0, + ); + next.r_hand.ori = + Quaternion::rotation_x(2.2) * Quaternion::rotation_y(-0.3 + drop * 0.1); + next.r_hand.scale = Vec3::one(); + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + 4.0 + skeleton_attr.foot.1, + 1.0 + skeleton_attr.foot.2 + drop * -2.0, + ); + next.l_foot.ori = Quaternion::rotation_x(0.55 + drop * 0.1); + next.l_foot.scale = Vec3::one(); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 2.0 + skeleton_attr.foot.1, + -2.0 + skeleton_attr.foot.2 + smooth * 1.0, + ); + next.r_foot.ori = Quaternion::rotation_x(0.2 + smooth * 0.15); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.ori = Quaternion::rotation_x(0.0); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.ori = Quaternion::rotation_x(0.0); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.main.offset = Vec3::new(-7.0, -5.0, 18.0); + next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.offset = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.control.scale = Vec3::one(); + + next.l_control.scale = Vec3::one(); + } + }; next.r_control.scale = Vec3::one(); next.second.scale = match ( diff --git a/voxygen/src/anim/src/character/run.rs b/voxygen/src/anim/src/character/run.rs index f85f1ca477..d821576b11 100644 --- a/voxygen/src/anim/src/character/run.rs +++ b/voxygen/src/anim/src/character/run.rs @@ -48,17 +48,17 @@ impl Animation for RunAnimation { let footvertl = (anim_time as f32 * 16.0 * walk * lab as f32).sin(); let footvertr = (anim_time as f32 * 16.0 * walk * lab as f32 + PI).sin(); - let footrotl = (((5.0) - / (2.5 - + (2.5) + let footrotl = (((1.0) + / (0.5 + + (0.5) * ((anim_time as f32 * 16.0 * walk * lab as f32 + PI * 1.4).sin()) .powf(2.0 as f32))) .sqrt()) * ((anim_time as f32 * 16.0 * walk * lab as f32 + PI * 1.4).sin()); - let footrotr = (((5.0) - / (1.0 - + (4.0) + let footrotr = (((1.0) + / (0.5 + + (0.5) * ((anim_time as f32 * 16.0 * walk * lab as f32 + PI * 0.4).sin()) .powf(2.0 as f32))) .sqrt()) diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index e7e23023ae..e505c83198 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -12,6 +12,7 @@ type SwimAnimationDependency = ( Vec3, Vec3, f64, + Vec3, ); impl Animation for SwimAnimation { @@ -25,17 +26,19 @@ impl Animation for SwimAnimation { fn update_skeleton_inner( skeleton: &Self::Skeleton, - (active_tool_kind, second_tool_kind, velocity, orientation, last_ori, global_time): Self::Dependency, + (active_tool_kind, second_tool_kind, velocity, orientation, last_ori, global_time, avg_vel): Self::Dependency, anim_time: f64, rate: &mut f32, skeleton_attr: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let speed = Vec2::::from(velocity).magnitude(); + let speed = Vec3::::from(velocity).magnitude(); *rate = 1.0; + let tempo = if speed > 0.5 { 1.0 } else { 0.7 }; + let intensity = if speed > 0.5 { 1.0 } else { 0.3 }; - let lab = 1.0; + let lab = 1.0 * tempo; let short = (anim_time as f32 * lab as f32 * 6.0).sin(); @@ -45,13 +48,29 @@ impl Animation for SwimAnimation { let wave_stop = (anim_time as f32 * 9.0).min(PI / 2.0 / 2.0).sin(); + let footrotl = (((1.0) + / (0.2 + + (0.8) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 1.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 1.4).sin()); + + let footrotr = (((1.0) + / (0.2 + + (0.8) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 0.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 0.4).sin()); + + let foothoril = (anim_time as f32 * 6.0 * lab as f32 + PI * 1.45).sin(); + let foothorir = (anim_time as f32 * 6.0 * lab as f32 + PI * (0.45)).sin(); let head_look = Vec2::new( - ((global_time + anim_time) as f32 / 18.0) + ((global_time + anim_time) as f32 / 4.0 * (1.0 / tempo)) .floor() .mul(7331.0) .sin() * 0.2, - ((global_time + anim_time) as f32 / 18.0) + ((global_time + anim_time) as f32 / 4.0 * (1.0 / tempo)) .floor() .mul(1337.0) .sin() @@ -70,25 +89,32 @@ impl Animation for SwimAnimation { } else { 0.0 } * 1.3; + + let adjust = if speed > 0.5 { -1.57 } else { -3.14 * speed }; next.head.offset = Vec3::new( 0.0, -3.0 + skeleton_attr.head.0, skeleton_attr.head.1 - 1.0 + short * 0.3, ); - next.head.ori = Quaternion::rotation_z(head_look.x - short * 0.4) - * Quaternion::rotation_x(head_look.y + 0.35 + speed * 0.045); + next.head.ori = Quaternion::rotation_z(head_look.x + short * -0.6 * intensity) + * Quaternion::rotation_x( + (0.6 * head_look.y * (1.0 / intensity)).abs() + + 0.45 * intensity + + velocity.z * 0.02, + ); next.head.scale = Vec3::one() * skeleton_attr.head_scale; next.chest.offset = Vec3::new( 0.0, skeleton_attr.chest.0, - skeleton_attr.chest.1 + short * 1.3, + -13.0 + skeleton_attr.chest.1 + short * 1.3 * intensity, ); - next.chest.ori = Quaternion::rotation_z(short * 0.4); + next.chest.ori = Quaternion::rotation_z(short * 0.4 * intensity); next.chest.scale = Vec3::one(); next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); - next.belt.ori = Quaternion::rotation_z(short * 0.30); + next.belt.ori = Quaternion::rotation_x(velocity.z * 0.01) + * Quaternion::rotation_z(short * 0.2 * intensity); next.belt.scale = Vec3::one(); next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); @@ -96,39 +122,42 @@ impl Animation for SwimAnimation { next.back.scale = Vec3::one() * 1.02; next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_z(short * 0.5); + next.shorts.ori = Quaternion::rotation_x(velocity.z * 0.02) + * Quaternion::rotation_z(short * 0.3 * intensity); next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( -skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 - foot * 1.2, - 2.0 + skeleton_attr.hand.2 + foot * -3.0, + 1.5 + skeleton_attr.hand.1 - foot * 1.2 * intensity, + 2.0 + skeleton_attr.hand.2 + foot * -3.0 * intensity, ); - next.l_hand.ori = Quaternion::rotation_x(0.8 + foot * -0.6) * Quaternion::rotation_y(0.2); + next.l_hand.ori = + Quaternion::rotation_x(0.8 + foot * -0.6 * intensity) * Quaternion::rotation_y(0.2); next.l_hand.scale = Vec3::one(); next.r_hand.offset = Vec3::new( skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 + foot * 1.2, - 2.0 + skeleton_attr.hand.2 + foot * 3.0, + 1.5 + skeleton_attr.hand.1 + foot * 1.2 * intensity, + 2.0 + skeleton_attr.hand.2 + foot * 3.0 * intensity, ); - next.r_hand.ori = Quaternion::rotation_x(0.8 + foot * 0.6) * Quaternion::rotation_y(-0.2); + next.r_hand.ori = + Quaternion::rotation_x(0.8 + foot * 0.6 * intensity) * Quaternion::rotation_y(-0.2); next.r_hand.scale = Vec3::one(); next.l_foot.offset = Vec3::new( -skeleton_attr.foot.0, - skeleton_attr.foot.1 + foot * 1.2, - -3.0 + skeleton_attr.foot.2 + foot * 3.5, + skeleton_attr.foot.1 + foothorir * 1.5 * intensity, + -15.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, ); - next.l_foot.ori = Quaternion::rotation_x(-1.1 + foot * 0.6); + next.l_foot.ori = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); next.l_foot.scale = Vec3::one(); next.r_foot.offset = Vec3::new( skeleton_attr.foot.0, - skeleton_attr.foot.1 - foot * 1.2, - -3.0 + skeleton_attr.foot.2 + foot * -3.5, + skeleton_attr.foot.1 + foothorir * 1.5 * intensity, + -15.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, ); - next.r_foot.ori = Quaternion::rotation_x(-1.1 + foot * -0.6); + next.r_foot.ori = Quaternion::rotation_x(-0.8 + footrotr * 0.4 * intensity); next.r_foot.scale = Vec3::one(); next.l_shoulder.offset = Vec3::new( @@ -136,7 +165,7 @@ impl Animation for SwimAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.l_shoulder.ori = Quaternion::rotation_x(short * 0.15); + next.l_shoulder.ori = Quaternion::rotation_x(short * 0.15 * intensity); next.l_shoulder.scale = Vec3::one() * 1.1; next.r_shoulder.offset = Vec3::new( @@ -144,7 +173,7 @@ impl Animation for SwimAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.r_shoulder.ori = Quaternion::rotation_x(short * -0.15); + next.r_shoulder.ori = Quaternion::rotation_x(short * -0.15 * intensity); next.r_shoulder.scale = Vec3::one() * 1.1; next.glider.offset = Vec3::new(0.0, 0.0, 10.0); @@ -194,9 +223,9 @@ impl Animation for SwimAnimation { next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); next.lantern.scale = Vec3::one() * 0.65; - next.torso.offset = Vec3::new(0.0, -1.2 + shortalt * -0.065, 0.4) * skeleton_attr.scaler; - next.torso.ori = Quaternion::rotation_x(speed * -0.190 * wave_stop * 1.05) - * Quaternion::rotation_z(tilt * 12.0); + next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(adjust + avg_vel.z * 0.12) + * Quaternion::rotation_z(tilt * 12.0 + short * 0.4 * intensity); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; next.control.scale = Vec3::one(); diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 980433f024..68cb4cd7b6 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -636,6 +636,7 @@ impl FigureMgr { ori, state.last_ori, time, + state.avg_vel, ), state.state_time, &mut state_animation_rate, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 3c5f266164..d59c543277 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -322,8 +322,11 @@ impl PlayState for SessionState { Event::InputUpdate(GameInput::Jump, state) => { self.inputs.jump.set_state(state); }, - Event::InputUpdate(GameInput::Swim, state) => { - self.inputs.swim.set_state(state); + Event::InputUpdate(GameInput::SwimUp, state) => { + self.inputs.swimup.set_state(state); + }, + Event::InputUpdate(GameInput::SwimDown, state) => { + self.inputs.swimdown.set_state(state); }, Event::InputUpdate(GameInput::Sit, state) if state != self.key_state.toggle_sit => diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 441108bdb7..4011998185 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -134,7 +134,8 @@ impl ControlSettings { GameInput::Glide => KeyMouse::Key(VirtualKeyCode::LShift), GameInput::Climb => KeyMouse::Key(VirtualKeyCode::Space), GameInput::ClimbDown => KeyMouse::Key(VirtualKeyCode::LControl), - GameInput::Swim => KeyMouse::Key(VirtualKeyCode::Space), + GameInput::SwimUp => KeyMouse::Key(VirtualKeyCode::Space), + GameInput::SwimDown => KeyMouse::Key(VirtualKeyCode::LShift), //GameInput::WallLeap => MIDDLE_CLICK_KEY, GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G), GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F), @@ -199,7 +200,8 @@ impl Default for ControlSettings { GameInput::Glide, GameInput::Climb, GameInput::ClimbDown, - GameInput::Swim, + GameInput::SwimUp, + GameInput::SwimDown, //GameInput::WallLeap, GameInput::ToggleLantern, GameInput::Mount, @@ -308,7 +310,8 @@ pub mod con_settings { pub glide: Button, pub climb: Button, pub climb_down: Button, - pub swim: Button, + pub swimup: Button, + pub swimdown: Button, //pub wall_leap: Button, pub toggle_lantern: Button, pub mount: Button, @@ -398,7 +401,8 @@ pub mod con_settings { glide: Button::Simple(GilButton::LeftTrigger), climb: Button::Simple(GilButton::South), climb_down: Button::Simple(GilButton::Unknown), - swim: Button::Simple(GilButton::South), + swimup: Button::Simple(GilButton::South), + swimdown: Button::Simple(GilButton::Unknown), //wall_leap: Button::Simple(GilButton::Unknown), toggle_lantern: Button::Simple(GilButton::East), mount: Button::Simple(GilButton::North), diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index b8bf69c08b..efa4702f4c 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -39,7 +39,8 @@ pub enum GameInput { Glide, Climb, ClimbDown, - Swim, + SwimUp, + SwimDown, //WallLeap, ToggleLantern, Mount, @@ -88,7 +89,8 @@ impl GameInput { GameInput::Glide => "gameinput.glide", GameInput::Climb => "gameinput.climb", GameInput::ClimbDown => "gameinput.climbdown", - GameInput::Swim => "gameinput.swim", + GameInput::SwimUp => "gameinput.swimup", + GameInput::SwimDown => "gameinput.swimdown", //GameInput::WallLeap => "gameinput.wallleap", GameInput::ToggleLantern => "gameinput.togglelantern", GameInput::Mount => "gameinput.mount", @@ -147,7 +149,8 @@ impl GameInput { GameInput::Glide, GameInput::Climb, GameInput::ClimbDown, - GameInput::Swim, + GameInput::SwimUp, + GameInput::SwimDown, GameInput::ToggleLantern, GameInput::Mount, GameInput::Enter, @@ -201,7 +204,7 @@ impl GameInput { match self { GameInput::Jump => GameInput::Jump, GameInput::Climb => GameInput::Jump, - GameInput::Swim => GameInput::Jump, + GameInput::SwimUp => GameInput::Jump, GameInput::Respawn => GameInput::Jump, GameInput::FreeLook => GameInput::FreeLook, From 9ff5c23cf04583e78f48a09b87c0ec60fb82c5bb Mon Sep 17 00:00:00 2001 From: jshipsey Date: Sun, 2 Aug 2020 01:09:11 -0400 Subject: [PATCH 60/71] readd sneak --- client/src/lib.rs | 15 + common/src/comp/character_state.rs | 1 + common/src/comp/controller.rs | 13 +- common/src/states/climb.rs | 7 +- common/src/states/glide_wield.rs | 11 +- common/src/states/idle.rs | 6 + common/src/states/mod.rs | 1 + common/src/states/sneak.rs | 56 ++++ common/src/states/utils.rs | 15 +- common/src/states/wielding.rs | 6 + common/src/sys/character_behavior.rs | 6 + common/src/sys/phys.rs | 2 +- common/src/sys/stats.rs | 1 + voxygen/src/anim/src/character/mod.rs | 6 +- voxygen/src/anim/src/character/sneak.rs | 319 +++++++++++++++++++ voxygen/src/anim/src/character/swim.rs | 6 +- voxygen/src/anim/src/quadruped_low/mod.rs | 2 +- voxygen/src/anim/src/quadruped_medium/mod.rs | 2 +- voxygen/src/key_state.rs | 2 + voxygen/src/scene/figure/mod.rs | 9 + voxygen/src/session.rs | 9 + voxygen/src/settings.rs | 4 + voxygen/src/window.rs | 3 + 23 files changed, 482 insertions(+), 20 deletions(-) create mode 100644 common/src/states/sneak.rs create mode 100644 voxygen/src/anim/src/character/sneak.rs diff --git a/client/src/lib.rs b/client/src/lib.rs index 404c2a1686..f4a864b2de 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -594,6 +594,21 @@ impl Client { } } + pub fn toggle_sneak(&mut self) { + let is_sneaking = self + .state + .ecs() + .read_storage::() + .get(self.entity) + .map(|cs| matches!(cs, comp::CharacterState::Sneak)); + + match is_sneaking { + Some(true) => self.control_action(ControlAction::Stand), + Some(false) => self.control_action(ControlAction::Sneak), + None => warn!("Can't toggle sneak, client entity doesn't have a `CharacterState`"), + } + } + pub fn toggle_glide(&mut self) { let is_gliding = self .state diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index a77263578d..af1d1fac4c 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -41,6 +41,7 @@ pub enum CharacterState { Climb, Sit, Dance, + Sneak, Glide, GlideWield, /// A basic blocking state diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 3c2c6c8828..ff00ac4b49 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -46,6 +46,7 @@ pub enum ControlAction { Unwield, Sit, Dance, + Sneak, Stand, } @@ -170,7 +171,8 @@ pub struct ControllerInputs { pub wall_leap: Input, pub charge: Input, pub climb: Option, - pub swim: Input, + pub swimup: Input, + pub swimdown: Input, pub move_dir: Vec2, pub look_dir: Dir, } @@ -194,7 +196,8 @@ impl ControllerInputs { self.glide.tick(dt); self.wall_leap.tick(dt); self.charge.tick(dt); - self.swim.tick(dt); + self.swimup.tick(dt); + self.swimdown.tick(dt); } pub fn tick_freshness(&mut self) { @@ -206,7 +209,8 @@ impl ControllerInputs { self.glide.tick_freshness(); self.wall_leap.tick_freshness(); self.charge.tick_freshness(); - self.swim.tick_freshness(); + self.swimup.tick_freshness(); + self.swimdown.tick_freshness(); } /// Updates Controller inputs with new version received from the client @@ -220,7 +224,8 @@ impl ControllerInputs { self.wall_leap.update_with_new(new.wall_leap); self.charge.update_with_new(new.charge); self.climb = new.climb; - self.swim.update_with_new(new.swim); + self.swimup.update_with_new(new.swimup); + self.swimdown.update_with_new(new.swimdown); self.move_dir = new.move_dir; self.look_dir = new.look_dir; } diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 3a5ca7093e..44642ec656 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -52,7 +52,8 @@ impl CharacterBehavior for Data { // Expend energy if climbing let energy_use = match climb { - Climb::Up | Climb::Down => 8, + Climb::Up => 5, + Climb::Down => 1, Climb::Hold => 1, }; @@ -79,14 +80,14 @@ impl CharacterBehavior for Data { match climb { Climb::Down => { update.vel.0 -= - data.dt.0 * update.vel.0.map(|e| e.abs().powf(1.5) * e.signum() * 6.0); + data.dt.0 * update.vel.0.map(|e| e.abs().powf(1.5) * e.signum() * 1.0); }, Climb::Up => { update.vel.0.z = (update.vel.0.z + data.dt.0 * GRAVITY * 1.25).min(CLIMB_SPEED); }, Climb::Hold => { // Antigrav - update.vel.0.z = (update.vel.0.z + data.dt.0 * GRAVITY * 1.5).min(CLIMB_SPEED); + update.vel.0.z = (update.vel.0.z + data.dt.0 * GRAVITY * 1.1).min(CLIMB_SPEED); update.vel.0 = Lerp::lerp( update.vel.0, Vec3::zero(), diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs index e385a3d3a0..0cc4d826e0 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -16,9 +16,12 @@ impl CharacterBehavior for Data { handle_wield(data, &mut update); // If not on the ground while wielding glider enter gliding state - if !data.physics.on_ground && !data.physics.in_fluid { + if !data.physics.on_ground { update.character = CharacterState::Glide; } + if data.physics.in_fluid { + update.character = CharacterState::Idle; + } update } @@ -35,6 +38,12 @@ impl CharacterBehavior for Data { update } + fn sneak(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sneak(data, &mut update); + update + } + fn unwield(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle; diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs index e86c1bd77c..fdc2dc1ef1 100644 --- a/common/src/states/idle.rs +++ b/common/src/states/idle.rs @@ -37,6 +37,12 @@ impl CharacterBehavior for Data { update } + fn sneak(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sneak(data, &mut update); + update + } + fn glide_wield(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); attempt_glide_wield(data, &mut update); diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index b88bfe7514..5c620b8c6b 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -13,6 +13,7 @@ pub mod idle; pub mod leap_melee; pub mod roll; pub mod sit; +pub mod sneak; pub mod spin_melee; pub mod triple_strike; pub mod utils; diff --git a/common/src/states/sneak.rs b/common/src/states/sneak.rs new file mode 100644 index 0000000000..9efbdadb2b --- /dev/null +++ b/common/src/states/sneak.rs @@ -0,0 +1,56 @@ +use super::utils::*; +use crate::{ + comp::{CharacterState, StateUpdate}, + sys::character_behavior::{CharacterBehavior, JoinData}, +}; + +pub struct Data; + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + handle_move(data, &mut update, 0.4); + handle_jump(data, &mut update); + handle_wield(data, &mut update); + handle_climb(data, &mut update); + handle_dodge_input(data, &mut update); + + // Try to Fall/Stand up/Move + if !data.physics.on_ground { + update.character = CharacterState::Idle; + } + + update + } + + fn wield(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_wield(data, &mut update); + update + } + + fn sit(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sit(data, &mut update); + update + } + + fn dance(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_dance(data, &mut update); + update + } + + fn glide_wield(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_glide_wield(data, &mut update); + update + } + + fn swap_loadout(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_swap_loadout(data, &mut update); + update + } +} diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ee20892e6c..7a2b4f19a1 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -118,9 +118,14 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { handle_orientation(data, update, if data.physics.on_ground { 9.0 } else { 2.0 }); // Swim - if data.inputs.swim.is_pressed() { + if data.inputs.swimup.is_pressed() { update.vel.0.z = - (update.vel.0.z + data.dt.0 * GRAVITY * 2.25).min(BASE_HUMANOID_WATER_SPEED); + (update.vel.0.z + data.dt.0 * GRAVITY * 4.0).min(BASE_HUMANOID_WATER_SPEED); + } + // Swim + if data.inputs.swimdown.is_pressed() { + update.vel.0.z = + (update.vel.0.z + data.dt.0 * GRAVITY * -3.5).min(BASE_HUMANOID_WATER_SPEED); } } @@ -159,6 +164,12 @@ pub fn attempt_dance(data: &JoinData, update: &mut StateUpdate) { } } +pub fn attempt_sneak(data: &JoinData, update: &mut StateUpdate) { + if data.physics.on_ground && data.body.is_humanoid() { + update.character = CharacterState::Sneak; + } +} + /// Checks that player can `Climb` and updates `CharacterState` if so pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) { if data.inputs.climb.is_some() diff --git a/common/src/states/wielding.rs b/common/src/states/wielding.rs index aa723fce2c..f64ae9c77f 100644 --- a/common/src/states/wielding.rs +++ b/common/src/states/wielding.rs @@ -33,6 +33,12 @@ impl CharacterBehavior for Data { update } + fn sneak(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sneak(data, &mut update); + update + } + fn unwield(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle; diff --git a/common/src/sys/character_behavior.rs b/common/src/sys/character_behavior.rs index e79d31aa29..eccf237433 100644 --- a/common/src/sys/character_behavior.rs +++ b/common/src/sys/character_behavior.rs @@ -27,6 +27,7 @@ pub trait CharacterBehavior { fn unwield(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn sit(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn dance(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } + fn sneak(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn stand(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) } fn handle_event(&self, data: &JoinData, event: ControlAction) -> StateUpdate { match event { @@ -36,6 +37,7 @@ pub trait CharacterBehavior { ControlAction::Unwield => self.unwield(data), ControlAction::Sit => self.sit(data), ControlAction::Dance => self.dance(data), + ControlAction::Sneak => self.sneak(data), ControlAction::Stand => self.stand(data), } } @@ -232,6 +234,9 @@ impl<'a> System<'a> for Sys { CharacterState::Dance => { states::dance::Data::handle_event(&states::dance::Data, &j, action) }, + CharacterState::Sneak => { + states::sneak::Data::handle_event(&states::sneak::Data, &j, action) + }, CharacterState::BasicBlock => { states::basic_block::Data.handle_event(&j, action) }, @@ -261,6 +266,7 @@ impl<'a> System<'a> for Sys { CharacterState::GlideWield => states::glide_wield::Data.behavior(&j), CharacterState::Sit => states::sit::Data::behavior(&states::sit::Data, &j), CharacterState::Dance => states::dance::Data::behavior(&states::dance::Data, &j), + CharacterState::Sneak => states::sneak::Data::behavior(&states::sneak::Data, &j), CharacterState::BasicBlock => states::basic_block::Data.behavior(&j), CharacterState::Roll(data) => data.behavior(&j), CharacterState::Wielding => states::wielding::Data.behavior(&j), diff --git a/common/src/sys/phys.rs b/common/src/sys/phys.rs index ffec0df1b4..0df848919c 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -15,7 +15,7 @@ use specs::{ use vek::*; pub const GRAVITY: f32 = 9.81 * 5.0; -const BOUYANCY: f32 = 0.0; +const BOUYANCY: f32 = 1.0; // Friction values used for linear damping. They are unitless quantities. The // value of these quantities must be between zero and one. They represent the // amount an object will slow down within 1/60th of a second. Eg. if the frction diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index 3c83d45bb9..100cd4170b 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -77,6 +77,7 @@ impl<'a> System<'a> for Sys { CharacterState::Idle { .. } | CharacterState::Sit { .. } | CharacterState::Dance { .. } + | CharacterState::Sneak { .. } | CharacterState::Glide { .. } | CharacterState::GlideWield { .. } | CharacterState::Wielding { .. } diff --git a/voxygen/src/anim/src/character/mod.rs b/voxygen/src/anim/src/character/mod.rs index f3440fa3ed..e0f1b635ce 100644 --- a/voxygen/src/anim/src/character/mod.rs +++ b/voxygen/src/anim/src/character/mod.rs @@ -16,6 +16,7 @@ pub mod roll; pub mod run; pub mod shoot; pub mod sit; +pub mod sneak; pub mod spin; pub mod spinmelee; pub mod stand; @@ -29,8 +30,9 @@ pub use self::{ dance::DanceAnimation, dash::DashAnimation, equip::EquipAnimation, glidewield::GlideWieldAnimation, gliding::GlidingAnimation, idle::IdleAnimation, jump::JumpAnimation, leapmelee::LeapAnimation, roll::RollAnimation, run::RunAnimation, - shoot::ShootAnimation, sit::SitAnimation, spin::SpinAnimation, spinmelee::SpinMeleeAnimation, - stand::StandAnimation, swim::SwimAnimation, wield::WieldAnimation, + shoot::ShootAnimation, sit::SitAnimation, sneak::SneakAnimation, spin::SpinAnimation, + spinmelee::SpinMeleeAnimation, stand::StandAnimation, swim::SwimAnimation, + wield::WieldAnimation, }; use super::{Bone, FigureBoneData, Skeleton}; diff --git a/voxygen/src/anim/src/character/sneak.rs b/voxygen/src/anim/src/character/sneak.rs new file mode 100644 index 0000000000..281607f259 --- /dev/null +++ b/voxygen/src/anim/src/character/sneak.rs @@ -0,0 +1,319 @@ +use super::{super::Animation, CharacterSkeleton, SkeletonAttr}; +use common::comp::item::ToolKind; +use std::{f32::consts::PI, ops::Mul}; +use vek::*; + +pub struct SneakAnimation; + +impl Animation for SneakAnimation { + type Dependency = (Option, Vec3, Vec3, Vec3, f64); + type Skeleton = CharacterSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"character_sneak\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "character_sneak")] + #[allow(clippy::identity_conversion)] // TODO: Pending review in #587 + + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (_active_tool_kind, velocity, orientation, last_ori, global_time): Self::Dependency, + anim_time: f64, + rate: &mut f32, + skeleton_attr: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + let speed = Vec2::::from(velocity).magnitude(); + *rate = 1.0; + let slow = (anim_time as f32 * 3.0).sin(); + let breathe = ((anim_time as f32 * 0.5).sin()).abs(); + let walkintensity = if speed > 5.0 { 1.0 } else { 0.45 }; + let lower = if speed > 5.0 { 0.0 } else { 1.0 }; + let _snapfoot = if speed > 5.0 { 1.1 } else { 2.0 }; + let lab = 1.0; + let foothoril = (anim_time as f32 * 7.0 * lab as f32 + PI * 1.45).sin(); + let foothorir = (anim_time as f32 * 7.0 * lab as f32 + PI * (0.45)).sin(); + + let footvertl = (anim_time as f32 * 7.0 * lab as f32).sin(); + let footvertr = (anim_time as f32 * 7.0 * lab as f32 + PI).sin(); + + let footrotl = (((5.0) + / (2.5 + + (2.5) + * ((anim_time as f32 * 7.0 * lab as f32 + PI * 1.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 7.0 * lab as f32 + PI * 1.4).sin()); + + let footrotr = (((5.0) + / (1.0 + + (4.0) + * ((anim_time as f32 * 7.0 * lab as f32 + PI * 0.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 7.0 * lab as f32 + PI * 0.4).sin()); + + let short = (anim_time as f32 * lab as f32 * 7.0).sin(); + let noisea = (anim_time as f32 * 11.0 + PI / 6.0).sin(); + let noiseb = (anim_time as f32 * 19.0 + PI / 4.0).sin(); + + let shorte = (((5.0) + / (4.0 + 1.0 * ((anim_time as f32 * lab as f32 * 7.0).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * lab as f32 * 7.0).sin()); + + let shortalt = (anim_time as f32 * lab as f32 * 7.0 + PI / 2.0).sin(); + + let head_look = Vec2::new( + ((global_time + anim_time) as f32 / 18.0) + .floor() + .mul(7331.0) + .sin() + * 0.2, + ((global_time + anim_time) as f32 / 18.0) + .floor() + .mul(1337.0) + .sin() + * 0.1, + ); + + let ori = Vec2::from(orientation); + let last_ori = Vec2::from(last_ori); + let tilt = if Vec2::new(ori, last_ori) + .map(|o| Vec2::::from(o).magnitude_squared()) + .map(|m| m > 0.001 && m.is_finite()) + .reduce_and() + && ori.angle_between(last_ori).is_finite() + { + ori.angle_between(last_ori).min(0.2) + * last_ori.determine_side(Vec2::zero(), ori).signum() + } else { + 0.0 + } * 1.3; + + if speed > 0.5 { + next.l_hand.offset = Vec3::new( + 1.0 - skeleton_attr.hand.0, + 4.0 + skeleton_attr.hand.1, + 1.0 + skeleton_attr.hand.2, + ); + next.l_hand.ori = Quaternion::rotation_x(1.0); + next.l_hand.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new( + -1.0 + skeleton_attr.hand.0, + -1.0 + skeleton_attr.hand.1, + skeleton_attr.hand.2, + ); + next.r_hand.ori = Quaternion::rotation_x(0.4); + next.r_hand.scale = Vec3::one(); + next.head.offset = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + -1.0 + skeleton_attr.head.1 + short * 0.06, + ); + next.head.ori = Quaternion::rotation_z(tilt * -2.5 + head_look.x * 0.2 - short * 0.06) + * Quaternion::rotation_x(head_look.y + 0.45); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + + next.chest.offset = Vec3::new( + 0.0, + skeleton_attr.chest.0, + -1.0 + skeleton_attr.chest.1 + shortalt * -0.5, + ); + next.chest.ori = Quaternion::rotation_z(0.3 + short * 0.08 + tilt * -0.2) + * Quaternion::rotation_y(tilt * 0.8) + * Quaternion::rotation_x(-0.5); + next.chest.scale = Vec3::one(); + + next.belt.offset = + Vec3::new(0.0, 0.5 + skeleton_attr.belt.0, 0.7 + skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_z(short * 0.1 + tilt * -1.1) + * Quaternion::rotation_y(tilt * 0.5) + * Quaternion::rotation_x(0.2); + next.belt.scale = Vec3::one(); + + next.glider.ori = Quaternion::rotation_x(0.0); + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = + Quaternion::rotation_x(-0.25 + short * 0.1 + noisea * 0.1 + noiseb * 0.1); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.offset = Vec3::new( + 0.0, + 1.0 + skeleton_attr.shorts.0, + 1.0 + skeleton_attr.shorts.1, + ); + next.shorts.ori = Quaternion::rotation_z(short * 0.16 + tilt * -1.5) + * Quaternion::rotation_y(tilt * 0.7) + * Quaternion::rotation_x(0.3); + next.shorts.scale = Vec3::one(); + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothoril * -10.5 * walkintensity - lower * 1.0, + 1.0 + skeleton_attr.foot.2 + ((footvertl * -1.7).max(-1.0)) * walkintensity, + ); + next.l_foot.ori = Quaternion::rotation_x(-0.2 + footrotl * -0.8 * walkintensity) + * Quaternion::rotation_y(tilt * 1.8); + next.l_foot.scale = Vec3::one(); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothorir * -10.5 * walkintensity - lower * 1.0, + 1.0 + skeleton_attr.foot.2 + ((footvertr * -1.7).max(-1.0)) * walkintensity, + ); + next.r_foot.ori = Quaternion::rotation_x(-0.2 + footrotr * -0.8 * walkintensity) + * Quaternion::rotation_y(tilt * 1.8); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.ori = Quaternion::rotation_x(short * 0.15 * walkintensity); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.ori = Quaternion::rotation_x(short * -0.15 * walkintensity); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.main.offset = Vec3::new(-7.0, -6.5, 15.0); + next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.scale = Vec3::one() * 0.0; + + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = + Quaternion::rotation_x(shorte * 0.2 + 0.4) * Quaternion::rotation_y(shorte * 0.1); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.offset = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.control.offset = Vec3::new(0.0, 0.0, 0.0); + next.control.ori = Quaternion::rotation_x(0.0); + next.control.scale = Vec3::one(); + + next.l_control.scale = Vec3::one(); + + next.r_control.scale = Vec3::one(); + } else { + next.head.offset = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + -2.0 + skeleton_attr.head.1 + slow * 0.1 + breathe * -0.05, + ); + next.head.ori = Quaternion::rotation_z(head_look.x) + * Quaternion::rotation_x(0.6 + head_look.y.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale + breathe * -0.05; + + next.chest.offset = Vec3::new( + 0.0, + skeleton_attr.chest.0, + -3.0 + skeleton_attr.chest.1 + slow * 0.1, + ); + next.chest.ori = Quaternion::rotation_x(-0.7); + next.chest.scale = Vec3::one() * 1.01 + breathe * 0.03; + + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_z(0.3 + head_look.x * -0.1); + next.belt.scale = Vec3::one() + breathe * -0.03; + + next.l_hand.offset = Vec3::new( + 1.0 - skeleton_attr.hand.0, + 5.0 + skeleton_attr.hand.1, + 0.0 + skeleton_attr.hand.2, + ); + next.l_hand.ori = Quaternion::rotation_x(1.35); + next.l_hand.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new( + -1.0 + skeleton_attr.hand.0, + skeleton_attr.hand.1, + skeleton_attr.hand.2, + ); + next.r_hand.ori = Quaternion::rotation_x(0.4); + next.r_hand.scale = Vec3::one(); + + next.glider.ori = Quaternion::rotation_x(0.35); + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_z(0.6 + head_look.x * -0.2); + next.shorts.scale = Vec3::one() + breathe * -0.03; + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + -6.0 + skeleton_attr.foot.1, + 1.0 + skeleton_attr.foot.2, + ); + next.l_foot.ori = Quaternion::rotation_x(-0.5); + next.l_foot.scale = Vec3::one(); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 4.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + next.r_foot.ori = Quaternion::rotation_x(0.0); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.scale = (Vec3::one() + breathe * -0.05) * 1.15; + + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.scale = (Vec3::one() + breathe * -0.05) * 1.15; + + next.main.offset = Vec3::new(-7.0, -5.0, 15.0); + next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = Quaternion::rotation_x(0.1) * Quaternion::rotation_y(0.1); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.offset = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.control.scale = Vec3::one(); + + next.l_control.scale = Vec3::one(); + + next.r_control.scale = Vec3::one(); + } + next + } +} diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index e505c83198..56369cac2b 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -42,12 +42,8 @@ impl Animation for SwimAnimation { let short = (anim_time as f32 * lab as f32 * 6.0).sin(); - let shortalt = (anim_time as f32 * lab as f32 * 6.0 + PI / 2.0).sin(); - let foot = (anim_time as f32 * lab as f32 * 6.0).sin(); - let wave_stop = (anim_time as f32 * 9.0).min(PI / 2.0 / 2.0).sin(); - let footrotl = (((1.0) / (0.2 + (0.8) @@ -146,7 +142,7 @@ impl Animation for SwimAnimation { next.l_foot.offset = Vec3::new( -skeleton_attr.foot.0, - skeleton_attr.foot.1 + foothorir * 1.5 * intensity, + skeleton_attr.foot.1 + foothoril * 1.5 * intensity, -15.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, ); next.l_foot.ori = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); diff --git a/voxygen/src/anim/src/quadruped_low/mod.rs b/voxygen/src/anim/src/quadruped_low/mod.rs index 577ce0acac..cc621e6667 100644 --- a/voxygen/src/anim/src/quadruped_low/mod.rs +++ b/voxygen/src/anim/src/quadruped_low/mod.rs @@ -142,7 +142,7 @@ impl<'a> From<&'a comp::quadruped_low::Body> for SkeletonAttr { (Tortoise, _) => (5.0, 1.0), (Rocksnapper, _) => (6.0, 0.5), (Pangolin, _) => (-0.5, 8.0), - (Maneater, _) => (6.0, 9.5), + (Maneater, _) => (7.0, 11.5), }, head_lower: match (body.species, body.body_type) { (Crocodile, _) => (8.0, 0.0), diff --git a/voxygen/src/anim/src/quadruped_medium/mod.rs b/voxygen/src/anim/src/quadruped_medium/mod.rs index d78069b986..4a1fefe2bf 100644 --- a/voxygen/src/anim/src/quadruped_medium/mod.rs +++ b/voxygen/src/anim/src/quadruped_medium/mod.rs @@ -209,7 +209,7 @@ impl<'a> From<&'a comp::quadruped_medium::Body> for SkeletonAttr { (Wolf, _) => (5.0, -3.0), (Frostfang, _) => (4.0, -3.0), (Mouflon, _) => (10.5, -4.0), - (Catoblepas, _) => (1.0, -6.0), + (Catoblepas, _) => (1.0, -4.0), (Bonerattler, _) => (3.0, -3.0), }, tail: match (body.species, body.body_type) { diff --git a/voxygen/src/key_state.rs b/voxygen/src/key_state.rs index 84992bc8d6..5d12a95888 100644 --- a/voxygen/src/key_state.rs +++ b/voxygen/src/key_state.rs @@ -10,6 +10,7 @@ pub struct KeyState { pub toggle_wield: bool, pub toggle_glide: bool, pub toggle_sit: bool, + pub toggle_sneak: bool, pub toggle_dance: bool, pub auto_walk: bool, pub swap_loadout: bool, @@ -30,6 +31,7 @@ impl Default for KeyState { toggle_wield: false, toggle_glide: false, toggle_sit: false, + toggle_sneak: false, toggle_dance: false, auto_walk: false, swap_loadout: false, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 68cb4cd7b6..916e007a74 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -720,6 +720,15 @@ impl FigureMgr { ) } }, + CharacterState::Sneak { .. } => { + anim::character::SneakAnimation::update_skeleton( + &CharacterSkeleton::new(), + (active_tool_kind, vel.0, ori, state.last_ori, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + }, CharacterState::Boost(_) => { anim::character::AlphaAnimation::update_skeleton( &target_base, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index d59c543277..2f5cf8df9e 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -347,6 +347,15 @@ impl PlayState for SessionState { self.client.borrow_mut().toggle_dance(); } } + Event::InputUpdate(GameInput::Sneak, state) + if state != self.key_state.toggle_sneak => + { + self.key_state.toggle_sneak = state; + if state { + self.stop_auto_walk(); + self.client.borrow_mut().toggle_sneak(); + } + } Event::InputUpdate(GameInput::MoveForward, state) => { if state && global_state.settings.gameplay.stop_auto_walk_on_input { self.stop_auto_walk(); diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 4011998185..e8ef4c38a3 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -136,6 +136,7 @@ impl ControlSettings { GameInput::ClimbDown => KeyMouse::Key(VirtualKeyCode::LControl), GameInput::SwimUp => KeyMouse::Key(VirtualKeyCode::Space), GameInput::SwimDown => KeyMouse::Key(VirtualKeyCode::LShift), + GameInput::Sneak => KeyMouse::Key(VirtualKeyCode::LControl), //GameInput::WallLeap => MIDDLE_CLICK_KEY, GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G), GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F), @@ -202,6 +203,7 @@ impl Default for ControlSettings { GameInput::ClimbDown, GameInput::SwimUp, GameInput::SwimDown, + GameInput::Sneak, //GameInput::WallLeap, GameInput::ToggleLantern, GameInput::Mount, @@ -312,6 +314,7 @@ pub mod con_settings { pub climb_down: Button, pub swimup: Button, pub swimdown: Button, + pub sneak: Button, //pub wall_leap: Button, pub toggle_lantern: Button, pub mount: Button, @@ -403,6 +406,7 @@ pub mod con_settings { climb_down: Button::Simple(GilButton::Unknown), swimup: Button::Simple(GilButton::South), swimdown: Button::Simple(GilButton::Unknown), + sneak: Button::Simple(GilButton::Unknown), //wall_leap: Button::Simple(GilButton::Unknown), toggle_lantern: Button::Simple(GilButton::East), mount: Button::Simple(GilButton::North), diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index efa4702f4c..b8a6f913c4 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -41,6 +41,7 @@ pub enum GameInput { ClimbDown, SwimUp, SwimDown, + Sneak, //WallLeap, ToggleLantern, Mount, @@ -91,6 +92,7 @@ impl GameInput { GameInput::ClimbDown => "gameinput.climbdown", GameInput::SwimUp => "gameinput.swimup", GameInput::SwimDown => "gameinput.swimdown", + GameInput::Sneak => "gameinput.sneak", //GameInput::WallLeap => "gameinput.wallleap", GameInput::ToggleLantern => "gameinput.togglelantern", GameInput::Mount => "gameinput.mount", @@ -151,6 +153,7 @@ impl GameInput { GameInput::ClimbDown, GameInput::SwimUp, GameInput::SwimDown, + GameInput::Sneak, GameInput::ToggleLantern, GameInput::Mount, GameInput::Enter, From 316546af222e463e220bccc2c2c62b5e48928842 Mon Sep 17 00:00:00 2001 From: jshipsey Date: Fri, 7 Aug 2020 01:45:01 -0400 Subject: [PATCH 61/71] bandaid fix for swim wield --- voxygen/src/anim/src/character/mod.rs | 3 +- voxygen/src/anim/src/character/swim.rs | 28 +- voxygen/src/anim/src/character/swimwield.rs | 426 ++++++++++++++++++++ voxygen/src/scene/figure/mod.rs | 18 +- 4 files changed, 457 insertions(+), 18 deletions(-) create mode 100644 voxygen/src/anim/src/character/swimwield.rs diff --git a/voxygen/src/anim/src/character/mod.rs b/voxygen/src/anim/src/character/mod.rs index e0f1b635ce..0e3d677e32 100644 --- a/voxygen/src/anim/src/character/mod.rs +++ b/voxygen/src/anim/src/character/mod.rs @@ -21,6 +21,7 @@ pub mod spin; pub mod spinmelee; pub mod stand; pub mod swim; +pub mod swimwield; pub mod wield; // Reexports @@ -31,7 +32,7 @@ pub use self::{ glidewield::GlideWieldAnimation, gliding::GlidingAnimation, idle::IdleAnimation, jump::JumpAnimation, leapmelee::LeapAnimation, roll::RollAnimation, run::RunAnimation, shoot::ShootAnimation, sit::SitAnimation, sneak::SneakAnimation, spin::SpinAnimation, - spinmelee::SpinMeleeAnimation, stand::StandAnimation, swim::SwimAnimation, + spinmelee::SpinMeleeAnimation, stand::StandAnimation, swim::SwimAnimation, swimwield::SwimWieldAnimation, wield::WieldAnimation, }; diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index 56369cac2b..7f0bc32e47 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -40,7 +40,7 @@ impl Animation for SwimAnimation { let lab = 1.0 * tempo; - let short = (anim_time as f32 * lab as f32 * 6.0).sin(); + let short = (anim_time as f32 * lab as f32 * 6.0+ PI/2.0).sin(); let foot = (anim_time as f32 * lab as f32 * 6.0).sin(); @@ -80,7 +80,7 @@ impl Animation for SwimAnimation { .reduce_and() && ori.angle_between(last_ori).is_finite() { - ori.angle_between(last_ori).min(0.2) + ori.angle_between(last_ori).min(0.8) * last_ori.determine_side(Vec2::zero(), ori).signum() } else { 0.0 @@ -92,9 +92,9 @@ impl Animation for SwimAnimation { -3.0 + skeleton_attr.head.0, skeleton_attr.head.1 - 1.0 + short * 0.3, ); - next.head.ori = Quaternion::rotation_z(head_look.x + short * -0.6 * intensity) + next.head.ori = Quaternion::rotation_z(head_look.x*0.5 + short * -0.2 * intensity) * Quaternion::rotation_x( - (0.6 * head_look.y * (1.0 / intensity)).abs() + (0.4 * head_look.y * (1.0 / intensity)).abs() + 0.45 * intensity + velocity.z * 0.02, ); @@ -105,7 +105,7 @@ impl Animation for SwimAnimation { skeleton_attr.chest.0, -13.0 + skeleton_attr.chest.1 + short * 1.3 * intensity, ); - next.chest.ori = Quaternion::rotation_z(short * 0.4 * intensity); + next.chest.ori = Quaternion::rotation_z(short * 0.1 * intensity); next.chest.scale = Vec3::one(); next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); @@ -123,21 +123,21 @@ impl Animation for SwimAnimation { next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( - -skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 - foot * 1.2 * intensity, - 2.0 + skeleton_attr.hand.2 + foot * -3.0 * intensity, + -1.0-skeleton_attr.hand.0, + 1.5 + skeleton_attr.hand.1 - foot * 2.0 * intensity, + 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity, ); next.l_hand.ori = - Quaternion::rotation_x(0.8 + foot * -0.6 * intensity) * Quaternion::rotation_y(0.2); + Quaternion::rotation_x(1.5 + foot * -1.2 * intensity) * Quaternion::rotation_y(0.4+foot*-0.35); next.l_hand.scale = Vec3::one(); next.r_hand.offset = Vec3::new( - skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 + foot * 1.2 * intensity, - 2.0 + skeleton_attr.hand.2 + foot * 3.0 * intensity, + 1.0+skeleton_attr.hand.0, + 1.5 + skeleton_attr.hand.1 + foot * 2.0 * intensity, + 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity, ); next.r_hand.ori = - Quaternion::rotation_x(0.8 + foot * 0.6 * intensity) * Quaternion::rotation_y(-0.2); + Quaternion::rotation_x(1.5 + foot * 1.2 * intensity) * Quaternion::rotation_y(-0.4+foot*-0.35); next.r_hand.scale = Vec3::one(); next.l_foot.offset = Vec3::new( @@ -221,7 +221,7 @@ impl Animation for SwimAnimation { next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; next.torso.ori = Quaternion::rotation_x(adjust + avg_vel.z * 0.12) - * Quaternion::rotation_z(tilt * 12.0 + short * 0.4 * intensity); + * Quaternion::rotation_z(tilt * 12.0 + short * 0.0 * intensity); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; next.control.scale = Vec3::one(); diff --git a/voxygen/src/anim/src/character/swimwield.rs b/voxygen/src/anim/src/character/swimwield.rs new file mode 100644 index 0000000000..a9000a7e2e --- /dev/null +++ b/voxygen/src/anim/src/character/swimwield.rs @@ -0,0 +1,426 @@ +use super::{super::Animation, CharacterSkeleton, SkeletonAttr}; +use common::comp::item::{Hands, ToolKind}; +use std::{f32::consts::PI, ops::Mul}; +use vek::*; + +pub struct SwimWieldAnimation; + +impl Animation for SwimWieldAnimation { + type Dependency = (Option, Option, f32, f64); + type Skeleton = CharacterSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"character_swimwield\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "character_swimwield")] + #[allow(clippy::approx_constant)] // TODO: Pending review in #587 + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (active_tool_kind, second_tool_kind, velocity, global_time): Self::Dependency, + anim_time: f64, + rate: &mut f32, + skeleton_attr: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + *rate = 1.0; + let lab = 1.0; + let speed = Vec3::::from(velocity).magnitude(); + *rate = 1.0; + let intensity = if speed > 0.5 { 1.0 } else { 0.3 }; + let footrotl = (((1.0) + / (0.2 + + (0.8) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 1.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 1.4).sin()); + + let footrotr = (((1.0) + / (0.2 + + (0.8) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 0.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * 6.0 * lab as f32 + PI * 0.4).sin()); + + let head_look = Vec2::new( + ((global_time + anim_time) as f32 / 3.0) + .floor() + .mul(7331.0) + .sin() + * 0.2, + ((global_time + anim_time) as f32 / 3.0) + .floor() + .mul(1337.0) + .sin() + * 0.1, + ); + + let slowalt = (anim_time as f32 * 6.0 + PI).cos(); + let u_slow = (anim_time as f32 * 1.0 + PI).sin(); + let slow = (anim_time as f32 * 3.0 + PI).sin(); + let foothoril = (anim_time as f32 * 6.0 * lab as f32 + PI * 1.45).sin(); + let foothorir = (anim_time as f32 * 6.0 * lab as f32 + PI * (0.45)).sin(); + let u_slowalt = (anim_time as f32 * 3.0 + PI).cos(); + let short = (((5.0) + / (1.5 + 3.5 * ((anim_time as f32 * lab as f32 * 16.0).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * lab as f32 * 16.0).sin()); + let noisea = (anim_time as f32 * 11.0 + PI / 6.0).sin(); + let noiseb = (anim_time as f32 * 19.0 + PI / 4.0).sin(); + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothoril * 1.5 * intensity, + -15.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, + ); + next.l_foot.ori = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); + next.l_foot.scale = Vec3::one(); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothorir * 1.5 * intensity, + -15.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, + ); + next.r_foot.ori = Quaternion::rotation_x(-0.8 + footrotr * 0.4 * intensity); + next.r_foot.scale = Vec3::one(); + if velocity > 0.1 { + next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(velocity * -0.05); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x( + (-0.5 + short * 0.3 + noisea * 0.3 + noiseb * 0.3).min(-0.1), + ); + next.back.scale = Vec3::one() * 1.02; + } else { + next.head.offset = Vec3::new( + 0.0, + -2.0 + skeleton_attr.head.0, + skeleton_attr.head.1 + u_slow * 0.1, + ); + next.head.ori = + Quaternion::rotation_z(head_look.x) * Quaternion::rotation_x(head_look.y.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + + next.chest.offset = Vec3::new( + 0.0 + slowalt * 0.5, + skeleton_attr.chest.0, + skeleton_attr.chest.1 + u_slow * 0.5, + ); + next.torso.offset = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + -2.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 2.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + + next.chest.ori = + Quaternion::rotation_y(u_slowalt * 0.04) * Quaternion::rotation_z(0.15); + + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_y(u_slowalt * 0.03) * Quaternion::rotation_z(0.22); + next.belt.scale = Vec3::one() * 1.02; + + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x(-0.2); + next.back.scale = Vec3::one() * 1.02; + next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_z(0.3); + } + match active_tool_kind { + //TODO: Inventory + Some(ToolKind::Sword(_)) => { + next.l_hand.offset = Vec3::new(-0.75, -1.0, -2.5); + next.l_hand.ori = Quaternion::rotation_x(1.47) * Quaternion::rotation_y(-0.2); + next.l_hand.scale = Vec3::one() * 1.04; + next.r_hand.offset = Vec3::new(0.75, -1.5, -5.5); + next.r_hand.ori = Quaternion::rotation_x(1.47) * Quaternion::rotation_y(0.3); + next.r_hand.scale = Vec3::one() * 1.05; + next.main.offset = Vec3::new(0.0, 0.0, -3.0); + next.main.ori = Quaternion::rotation_x(-0.1) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + + next.control.offset = Vec3::new(-7.0, 6.0, 6.0); + next.control.ori = Quaternion::rotation_x(u_slow * 0.15) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(u_slowalt * 0.08); + next.control.scale = Vec3::one(); + }, + Some(ToolKind::Dagger(_)) => { + // hands should be larger when holding a dagger grip, + // also reduce flicker with overlapping polygons + let hand_scale = 1.12; + + next.control.offset = Vec3::new(0.0, 0.0, 0.0); + //next.control.ori = Quaternion::rotation_x(slow * 1.0); + // * Quaternion::rotation_y(0.0) + // * Quaternion::rotation_z(u_slowalt * 0.08); + // next.control.scale = Vec3::one(); + + next.l_hand.offset = Vec3::new(0.0, 0.0, 0.0); + next.l_hand.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.l_hand.scale = Vec3::one() * hand_scale; + + next.main.offset = Vec3::new(0.0, 0.0, 0.0); + next.main.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + + next.l_control.offset = Vec3::new(-7.0, 0.0, 0.0); + // next.l_control.ori = Quaternion::rotation_x(u_slow * 0.15 + 1.0) + // * Quaternion::rotation_y(0.0) + // * Quaternion::rotation_z(u_slowalt * 0.08); + // next.l_control.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new(0.0, 0.0, 0.0); + next.r_hand.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.r_hand.scale = Vec3::one() * hand_scale; + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.second.scale = Vec3::one(); + + next.r_control.offset = Vec3::new(7.0, 0.0, 0.0); + // next.r_control.ori = Quaternion::rotation_x(0.0 * PI) + // * Quaternion::rotation_y(0.0 * PI) + // * Quaternion::rotation_z(0.0 * PI); + // next.r_control.scale = Vec3::one(); + }, + Some(ToolKind::Axe(_)) => { + if velocity < 0.5 { + next.head.offset = Vec3::new( + 0.0, + -3.5 + skeleton_attr.head.0, + skeleton_attr.head.1 + u_slow * 0.1, + ); + next.head.ori = Quaternion::rotation_z(head_look.x) + * Quaternion::rotation_x(0.35 + head_look.y.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + next.chest.ori = Quaternion::rotation_x(-0.35) + * Quaternion::rotation_y(u_slowalt * 0.04) + * Quaternion::rotation_z(0.15); + next.belt.offset = + Vec3::new(0.0, 1.0 + skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_x(0.15) + * Quaternion::rotation_y(u_slowalt * 0.03) + * Quaternion::rotation_z(0.15); + next.shorts.offset = + Vec3::new(0.0, 1.0 + skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_x(0.15) * Quaternion::rotation_z(0.25); + next.control.ori = Quaternion::rotation_x(1.8) + * Quaternion::rotation_y(-0.5) + * Quaternion::rotation_z(PI - 0.2); + next.control.scale = Vec3::one(); + } else { + next.control.ori = Quaternion::rotation_x(2.1) + * Quaternion::rotation_y(-0.4) + * Quaternion::rotation_z(PI - 0.2); + next.control.scale = Vec3::one(); + } + next.l_hand.offset = Vec3::new(-0.5, 0.0, 4.0); + next.l_hand.ori = Quaternion::rotation_x(PI / 2.0) + * Quaternion::rotation_z(0.0) + * Quaternion::rotation_y(0.0); + next.l_hand.scale = Vec3::one() * 1.08; + next.r_hand.offset = Vec3::new(0.5, 0.0, -2.5); + next.r_hand.ori = Quaternion::rotation_x(PI / 2.0) + * Quaternion::rotation_z(0.0) + * Quaternion::rotation_y(0.0); + next.r_hand.scale = Vec3::one() * 1.06; + next.main.offset = Vec3::new(-0.0, -2.0, -1.0); + next.main.ori = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + + next.control.offset = Vec3::new(-3.0, 11.0, 3.0); + }, + Some(ToolKind::Hammer(_)) => { + next.l_hand.offset = Vec3::new(-12.0, 0.0, 0.0); + next.l_hand.ori = Quaternion::rotation_x(-0.0) * Quaternion::rotation_y(0.0); + next.l_hand.scale = Vec3::one() * 1.08; + next.r_hand.offset = Vec3::new(2.0, 0.0, 0.0); + next.r_hand.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.r_hand.scale = Vec3::one() * 1.06; + next.main.offset = Vec3::new(0.0, 0.0, 0.0); + next.main.ori = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(-1.57) + * Quaternion::rotation_z(1.57); + + next.control.offset = Vec3::new(6.0, 7.0, 1.0); + next.control.ori = Quaternion::rotation_x(0.3 + u_slow * 0.15) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(u_slowalt * 0.08); + next.control.scale = Vec3::one(); + }, + Some(ToolKind::Staff(_)) => { + next.l_hand.offset = Vec3::new(1.5, 0.5, -4.0); + next.l_hand.ori = Quaternion::rotation_x(1.47) * Quaternion::rotation_y(-0.3); + next.l_hand.scale = Vec3::one() * 1.05; + next.r_hand.offset = Vec3::new(8.0, 4.0, 2.0); + next.r_hand.ori = Quaternion::rotation_x(1.8) + * Quaternion::rotation_y(0.5) + * Quaternion::rotation_z(-0.27); + next.r_hand.scale = Vec3::one() * 1.05; + next.main.offset = Vec3::new(12.0, 8.4, 13.2); + next.main.ori = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(3.14 + 0.3) + * Quaternion::rotation_z(0.9); + + next.control.offset = Vec3::new(-14.0, 1.8, 3.0); + next.control.ori = Quaternion::rotation_x(u_slow * 0.2) + * Quaternion::rotation_y(-0.2) + * Quaternion::rotation_z(u_slowalt * 0.1); + next.control.scale = Vec3::one(); + }, + Some(ToolKind::Shield(_)) => { + // hands should be larger when holding a dagger grip, + // also reduce flicker with overlapping polygons + let hand_scale = 1.12; + + next.control.offset = Vec3::new(0.0, 0.0, 0.0); + // next.control.ori = Quaternion::rotation_x(u_slow * 0.15 + 1.0) + // * Quaternion::rotation_y(0.0) + // * Quaternion::rotation_z(u_slowalt * 0.08); + // next.control.scale = Vec3::one(); + + next.l_hand.offset = Vec3::new(0.0, 0.0, 0.0); + next.l_hand.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.l_hand.scale = Vec3::one() * hand_scale; + + next.main.offset = Vec3::new(0.0, 0.0, 0.0); + next.main.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + + next.l_control.offset = Vec3::new(-7.0, 0.0, 0.0); + // next.l_control.ori = Quaternion::rotation_x(u_slow * 0.15 + 1.0) + // * Quaternion::rotation_y(0.0) + // * Quaternion::rotation_z(u_slowalt * 0.08); + // next.l_control.scale = Vec3::one(); + + next.r_hand.offset = Vec3::new(0.0, 0.0, 0.0); + next.r_hand.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.r_hand.scale = Vec3::one() * hand_scale; + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + next.second.scale = Vec3::one(); + + next.r_control.offset = Vec3::new(7.0, 0.0, 0.0); + // next.r_control.ori = Quaternion::rotation_x(0.0 * PI) + // * Quaternion::rotation_y(0.0 * PI) + // * Quaternion::rotation_z(0.0 * PI); + // next.r_control.scale = Vec3::one(); + }, + Some(ToolKind::Bow(_)) => { + next.l_hand.offset = Vec3::new(2.0, 1.5, 0.0); + next.l_hand.ori = Quaternion::rotation_x(1.20) + * Quaternion::rotation_y(-0.6) + * Quaternion::rotation_z(-0.3); + next.l_hand.scale = Vec3::one() * 1.05; + next.r_hand.offset = Vec3::new(5.9, 4.5, -5.0); + next.r_hand.ori = Quaternion::rotation_x(1.20) + * Quaternion::rotation_y(-0.6) + * Quaternion::rotation_z(-0.3); + next.r_hand.scale = Vec3::one() * 1.05; + next.main.offset = Vec3::new(3.0, 2.0, -13.0); + next.main.ori = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(0.3) + * Quaternion::rotation_z(-0.6); + + next.hold.offset = Vec3::new(1.2, -1.0, -5.2); + next.hold.ori = Quaternion::rotation_x(-1.7) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(-0.1); + next.hold.scale = Vec3::one() * 1.0; + + next.control.offset = Vec3::new(-7.0, 6.0, 6.0); + next.control.ori = + Quaternion::rotation_x(u_slow * 0.2) * Quaternion::rotation_z(u_slowalt * 0.1); + next.control.scale = Vec3::one(); + }, + Some(ToolKind::Debug(_)) => { + next.l_hand.offset = Vec3::new(-7.0, 4.0, 3.0); + next.l_hand.ori = Quaternion::rotation_x(1.27) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + next.l_hand.scale = Vec3::one() * 1.01; + next.r_hand.offset = Vec3::new(7.0, 2.5, -1.25); + next.r_hand.ori = Quaternion::rotation_x(1.27) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(-0.3); + next.r_hand.scale = Vec3::one() * 1.01; + next.main.offset = Vec3::new(5.0, 8.75, -2.0); + next.main.ori = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(-1.27) + * Quaternion::rotation_z(0.0); + next.main.scale = Vec3::one(); + next.control.offset = Vec3::new(0.0, 6.0, 6.0); + next.control.ori = + Quaternion::rotation_x(u_slow * 0.2) * Quaternion::rotation_z(u_slowalt * 0.1); + next.control.scale = Vec3::one(); + }, + Some(ToolKind::Farming(_)) => { + if velocity < 0.5 { + next.head.ori = Quaternion::rotation_z(head_look.x) + * Quaternion::rotation_x(-0.2 + head_look.y.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + } + next.l_hand.offset = Vec3::new(9.0, 1.0, 1.0); + next.l_hand.ori = Quaternion::rotation_x(1.57) * Quaternion::rotation_y(0.0); + next.l_hand.scale = Vec3::one() * 1.05; + next.r_hand.offset = Vec3::new(9.0, 1.0, 11.0); + next.r_hand.ori = Quaternion::rotation_x(1.57) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + next.r_hand.scale = Vec3::one() * 1.05; + next.main.offset = Vec3::new(7.5, 7.5, 13.2); + next.main.ori = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(3.14) + * Quaternion::rotation_z(0.0); + + next.control.offset = Vec3::new(-11.0 + slow * 2.0, 1.8, 4.0); + next.control.ori = Quaternion::rotation_x(u_slow * 0.1) + * Quaternion::rotation_y(0.6 + u_slow * 0.1) + * Quaternion::rotation_z(u_slowalt * 0.1); + next.control.scale = Vec3::one(); + }, + _ => {}, + } + + next.l_control.scale = Vec3::one(); + + next.r_control.scale = Vec3::one(); + + next.second.scale = match ( + active_tool_kind.map(|tk| tk.hands()), + second_tool_kind.map(|tk| tk.hands()), + ) { + (Some(Hands::OneHand), Some(Hands::OneHand)) => Vec3::one(), + (_, _) => Vec3::zero(), + }; + + next + } +} diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 916e007a74..541d88b19f 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -597,7 +597,7 @@ impl FigureMgr { skeleton_attr, ), // Running - (true, true, _) => anim::character::RunAnimation::update_skeleton( + (true, true, false) => anim::character::RunAnimation::update_skeleton( &CharacterSkeleton::new(), ( active_tool_kind.clone(), @@ -627,7 +627,7 @@ impl FigureMgr { skeleton_attr, ), // Swim - (false, _, true) => anim::character::SwimAnimation::update_skeleton( + (_, _, true) => anim::character::SwimAnimation::update_skeleton( &CharacterSkeleton::new(), ( active_tool_kind.clone(), @@ -823,13 +823,25 @@ impl FigureMgr { ) }, CharacterState::Wielding { .. } => { + if physics.in_fluid { + anim::character::SwimWieldAnimation::update_skeleton( + &target_base, + (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + } + else{ anim::character::WieldAnimation::update_skeleton( &target_base, (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, - ) +) + } + }, CharacterState::Glide { .. } => { anim::character::GlidingAnimation::update_skeleton( From 854361cdaa42eee6f3bbca223c53caae05429f5d Mon Sep 17 00:00:00 2001 From: jshipsey Date: Fri, 7 Aug 2020 01:51:50 -0400 Subject: [PATCH 62/71] changelog --- voxygen/src/anim/Cargo.toml | 2 +- voxygen/src/anim/src/character/mod.rs | 4 ++-- voxygen/src/anim/src/character/swim.rs | 16 ++++++------- voxygen/src/scene/figure/mod.rs | 32 ++++++++++++-------------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/voxygen/src/anim/Cargo.toml b/voxygen/src/anim/Cargo.toml index 92ce71acb0..ab4d0e2bfe 100644 --- a/voxygen/src/anim/Cargo.toml +++ b/voxygen/src/anim/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" name = "voxygen_anim" # Uncomment to use animation hot reloading # Note: this breaks `cargo test` -crate-type = ["lib", "cdylib"] +# crate-type = ["lib", "cdylib"] [features] use-dyn-lib = ["libloading", "notify", "lazy_static", "tracing", "find_folder"] diff --git a/voxygen/src/anim/src/character/mod.rs b/voxygen/src/anim/src/character/mod.rs index 0e3d677e32..7f2ef17a49 100644 --- a/voxygen/src/anim/src/character/mod.rs +++ b/voxygen/src/anim/src/character/mod.rs @@ -32,8 +32,8 @@ pub use self::{ glidewield::GlideWieldAnimation, gliding::GlidingAnimation, idle::IdleAnimation, jump::JumpAnimation, leapmelee::LeapAnimation, roll::RollAnimation, run::RunAnimation, shoot::ShootAnimation, sit::SitAnimation, sneak::SneakAnimation, spin::SpinAnimation, - spinmelee::SpinMeleeAnimation, stand::StandAnimation, swim::SwimAnimation, swimwield::SwimWieldAnimation, - wield::WieldAnimation, + spinmelee::SpinMeleeAnimation, stand::StandAnimation, swim::SwimAnimation, + swimwield::SwimWieldAnimation, wield::WieldAnimation, }; use super::{Bone, FigureBoneData, Skeleton}; diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index 7f0bc32e47..75ea32dfa2 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -40,7 +40,7 @@ impl Animation for SwimAnimation { let lab = 1.0 * tempo; - let short = (anim_time as f32 * lab as f32 * 6.0+ PI/2.0).sin(); + let short = (anim_time as f32 * lab as f32 * 6.0 + PI / 2.0).sin(); let foot = (anim_time as f32 * lab as f32 * 6.0).sin(); @@ -92,7 +92,7 @@ impl Animation for SwimAnimation { -3.0 + skeleton_attr.head.0, skeleton_attr.head.1 - 1.0 + short * 0.3, ); - next.head.ori = Quaternion::rotation_z(head_look.x*0.5 + short * -0.2 * intensity) + next.head.ori = Quaternion::rotation_z(head_look.x * 0.5 + short * -0.2 * intensity) * Quaternion::rotation_x( (0.4 * head_look.y * (1.0 / intensity)).abs() + 0.45 * intensity @@ -123,21 +123,21 @@ impl Animation for SwimAnimation { next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( - -1.0-skeleton_attr.hand.0, + -1.0 - skeleton_attr.hand.0, 1.5 + skeleton_attr.hand.1 - foot * 2.0 * intensity, 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity, ); - next.l_hand.ori = - Quaternion::rotation_x(1.5 + foot * -1.2 * intensity) * Quaternion::rotation_y(0.4+foot*-0.35); + next.l_hand.ori = Quaternion::rotation_x(1.5 + foot * -1.2 * intensity) + * Quaternion::rotation_y(0.4 + foot * -0.35); next.l_hand.scale = Vec3::one(); next.r_hand.offset = Vec3::new( - 1.0+skeleton_attr.hand.0, + 1.0 + skeleton_attr.hand.0, 1.5 + skeleton_attr.hand.1 + foot * 2.0 * intensity, 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity, ); - next.r_hand.ori = - Quaternion::rotation_x(1.5 + foot * 1.2 * intensity) * Quaternion::rotation_y(-0.4+foot*-0.35); + next.r_hand.ori = Quaternion::rotation_x(1.5 + foot * 1.2 * intensity) + * Quaternion::rotation_y(-0.4 + foot * -0.35); next.r_hand.scale = Vec3::one(); next.l_foot.offset = Vec3::new( diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 541d88b19f..68c0861f2b 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -824,24 +824,22 @@ impl FigureMgr { }, CharacterState::Wielding { .. } => { if physics.in_fluid { - anim::character::SwimWieldAnimation::update_skeleton( - &target_base, - (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), - state.state_time, - &mut state_animation_rate, - skeleton_attr, - ) + anim::character::SwimWieldAnimation::update_skeleton( + &target_base, + (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + } else { + anim::character::WieldAnimation::update_skeleton( + &target_base, + (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) } - else{ - anim::character::WieldAnimation::update_skeleton( - &target_base, - (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), - state.state_time, - &mut state_animation_rate, - skeleton_attr, -) - } - }, CharacterState::Glide { .. } => { anim::character::GlidingAnimation::update_skeleton( From be3ed4b238ee33dfb0874d7255bb2f781063de04 Mon Sep 17 00:00:00 2001 From: jshipsey Date: Fri, 7 Aug 2020 02:01:34 -0400 Subject: [PATCH 63/71] minor anim tweak --- voxygen/src/anim/src/character/swim.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index 75ea32dfa2..d7a1a832a2 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -125,7 +125,7 @@ impl Animation for SwimAnimation { next.l_hand.offset = Vec3::new( -1.0 - skeleton_attr.hand.0, 1.5 + skeleton_attr.hand.1 - foot * 2.0 * intensity, - 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity, + intensity * 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity, ); next.l_hand.ori = Quaternion::rotation_x(1.5 + foot * -1.2 * intensity) * Quaternion::rotation_y(0.4 + foot * -0.35); @@ -134,7 +134,7 @@ impl Animation for SwimAnimation { next.r_hand.offset = Vec3::new( 1.0 + skeleton_attr.hand.0, 1.5 + skeleton_attr.hand.1 + foot * 2.0 * intensity, - 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity, + intensity * 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity, ); next.r_hand.ori = Quaternion::rotation_x(1.5 + foot * 1.2 * intensity) * Quaternion::rotation_y(-0.4 + foot * -0.35); From d7b3e84153818a4b927a36a1578e7b700afe1ca3 Mon Sep 17 00:00:00 2001 From: jshipsey Date: Sat, 8 Aug 2020 13:47:17 -0400 Subject: [PATCH 64/71] swim alterations to condense the body on strong turns --- voxygen/src/anim/src/character/swim.rs | 79 ++++++++++++--------- voxygen/src/anim/src/character/swimwield.rs | 8 +-- voxygen/src/scene/figure/mod.rs | 2 +- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index d7a1a832a2..939eda7fd4 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -32,17 +32,19 @@ impl Animation for SwimAnimation { skeleton_attr: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); + let avgspeed = Vec2::::from(avg_vel).magnitude(); + let avgtotal = Vec3::::from(avg_vel).magnitude(); let speed = Vec3::::from(velocity).magnitude(); *rate = 1.0; - let tempo = if speed > 0.5 { 1.0 } else { 0.7 }; + let tempo = if speed > 0.5 { 1.5 } else { 0.7 }; let intensity = if speed > 0.5 { 1.0 } else { 0.3 }; let lab = 1.0 * tempo; - let short = (anim_time as f32 * lab as f32 * 6.0 + PI / 2.0).sin(); + let short = (anim_time as f32 * lab as f32 * 6.0 + PI * 0.9).sin(); - let foot = (anim_time as f32 * lab as f32 * 6.0).sin(); + let foot = (anim_time as f32 * lab as f32 * 6.0 + PI * -0.1).sin(); let footrotl = (((1.0) / (0.2 @@ -58,8 +60,8 @@ impl Animation for SwimAnimation { .sqrt()) * ((anim_time as f32 * 6.0 * lab as f32 + PI * 0.4).sin()); - let foothoril = (anim_time as f32 * 6.0 * lab as f32 + PI * 1.45).sin(); - let foothorir = (anim_time as f32 * 6.0 * lab as f32 + PI * (0.45)).sin(); + let foothoril = (anim_time as f32 * 6.0 * lab as f32 + PI * 1.4).sin(); + let foothorir = (anim_time as f32 * 6.0 * lab as f32 + PI * (0.4)).sin(); let head_look = Vec2::new( ((global_time + anim_time) as f32 / 4.0 * (1.0 / tempo)) .floor() @@ -85,32 +87,35 @@ impl Animation for SwimAnimation { } else { 0.0 } * 1.3; + let abstilt = tilt.abs(); - let adjust = if speed > 0.5 { -1.57 } else { -3.14 * speed }; + let squash = if abstilt > 0.2 { 0.35 } else { 1.0 }; //condenses the body at strong turns next.head.offset = Vec3::new( 0.0, -3.0 + skeleton_attr.head.0, skeleton_attr.head.1 - 1.0 + short * 0.3, ); - next.head.ori = Quaternion::rotation_z(head_look.x * 0.5 + short * -0.2 * intensity) - * Quaternion::rotation_x( - (0.4 * head_look.y * (1.0 / intensity)).abs() - + 0.45 * intensity - + velocity.z * 0.02, - ); + next.head.ori = + Quaternion::rotation_z(head_look.x * 0.5 + short * -0.2 * intensity + tilt * 3.0) + * Quaternion::rotation_x( + (0.4 * head_look.y * (1.0 / intensity)).abs() + + 0.45 * intensity + + velocity.z * 0.03 + - (abstilt * 1.8), + ); next.head.scale = Vec3::one() * skeleton_attr.head_scale; next.chest.offset = Vec3::new( 0.0, skeleton_attr.chest.0, - -13.0 + skeleton_attr.chest.1 + short * 1.3 * intensity, + -10.0 + skeleton_attr.chest.1 + short * 0.3 * intensity, ); next.chest.ori = Quaternion::rotation_z(short * 0.1 * intensity); next.chest.scale = Vec3::one(); next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); - next.belt.ori = Quaternion::rotation_x(velocity.z * 0.01) - * Quaternion::rotation_z(short * 0.2 * intensity); + next.belt.ori = Quaternion::rotation_x(velocity.z.abs() * -0.005 + abstilt * 1.0) + * Quaternion::rotation_z(short * -0.2 * intensity); next.belt.scale = Vec3::one(); next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); @@ -118,42 +123,44 @@ impl Animation for SwimAnimation { next.back.scale = Vec3::one() * 1.02; next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_x(velocity.z * 0.02) - * Quaternion::rotation_z(short * 0.3 * intensity); + next.shorts.ori = Quaternion::rotation_x(velocity.z.abs() * -0.005 + abstilt * 1.0) + * Quaternion::rotation_z(short * -0.3 * intensity); next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( -1.0 - skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 - foot * 2.0 * intensity, - intensity * 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity, + 1.5 + skeleton_attr.hand.1 - foot * 2.0 * intensity * squash, + intensity * 5.0 + skeleton_attr.hand.2 + foot * -5.0 * intensity * squash, ); - next.l_hand.ori = Quaternion::rotation_x(1.5 + foot * -1.2 * intensity) + next.l_hand.ori = Quaternion::rotation_x(1.5 + foot * -1.2 * intensity * squash) * Quaternion::rotation_y(0.4 + foot * -0.35); next.l_hand.scale = Vec3::one(); next.r_hand.offset = Vec3::new( 1.0 + skeleton_attr.hand.0, - 1.5 + skeleton_attr.hand.1 + foot * 2.0 * intensity, - intensity * 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity, + 1.5 + skeleton_attr.hand.1 + foot * 2.0 * intensity * squash, + intensity * 5.0 + skeleton_attr.hand.2 + foot * 5.0 * intensity * squash, ); - next.r_hand.ori = Quaternion::rotation_x(1.5 + foot * 1.2 * intensity) + next.r_hand.ori = Quaternion::rotation_x(1.5 + foot * 1.2 * intensity * squash) * Quaternion::rotation_y(-0.4 + foot * -0.35); next.r_hand.scale = Vec3::one(); next.l_foot.offset = Vec3::new( -skeleton_attr.foot.0, - skeleton_attr.foot.1 + foothoril * 1.5 * intensity, - -15.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, + skeleton_attr.foot.1 + foothoril * 1.5 * intensity * squash, + -10.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity * squash, ); - next.l_foot.ori = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); + next.l_foot.ori = + Quaternion::rotation_x(-0.8 * squash + footrotl * 0.4 * intensity * squash); next.l_foot.scale = Vec3::one(); next.r_foot.offset = Vec3::new( skeleton_attr.foot.0, - skeleton_attr.foot.1 + foothorir * 1.5 * intensity, - -15.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, + skeleton_attr.foot.1 + foothorir * 1.5 * intensity * squash, + -10.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity * squash, ); - next.r_foot.ori = Quaternion::rotation_x(-0.8 + footrotr * 0.4 * intensity); + next.r_foot.ori = + Quaternion::rotation_x(-0.8 * squash + footrotr * 0.4 * intensity * squash); next.r_foot.scale = Vec3::one(); next.l_shoulder.offset = Vec3::new( @@ -218,13 +225,19 @@ impl Animation for SwimAnimation { ); next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); next.lantern.scale = Vec3::one() * 0.65; - + let switch = if avg_vel.z > 0.0 && avgspeed < 0.5 { + avgtotal.min(0.5) + } else { + avgtotal + }; next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; - next.torso.ori = Quaternion::rotation_x(adjust + avg_vel.z * 0.12) - * Quaternion::rotation_z(tilt * 12.0 + short * 0.0 * intensity); + next.torso.ori = Quaternion::rotation_x( + (((1.0 / switch) * PI / 2.0 + avg_vel.z * 0.12).min(1.57) - PI / 2.0) + + avgspeed * avg_vel.z * -0.003, + ) * Quaternion::rotation_z(tilt * 8.0); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; - next.control.scale = Vec3::one(); + next.control.scale = Vec3::one(); //avgspeed*-0.14*reverse + next.l_control.scale = Vec3::one(); diff --git a/voxygen/src/anim/src/character/swimwield.rs b/voxygen/src/anim/src/character/swimwield.rs index a9000a7e2e..3554ffa5e5 100644 --- a/voxygen/src/anim/src/character/swimwield.rs +++ b/voxygen/src/anim/src/character/swimwield.rs @@ -70,7 +70,7 @@ impl Animation for SwimWieldAnimation { next.l_foot.offset = Vec3::new( -skeleton_attr.foot.0, skeleton_attr.foot.1 + foothoril * 1.5 * intensity, - -15.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, + -10.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, ); next.l_foot.ori = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); next.l_foot.scale = Vec3::one(); @@ -78,11 +78,11 @@ impl Animation for SwimWieldAnimation { next.r_foot.offset = Vec3::new( skeleton_attr.foot.0, skeleton_attr.foot.1 + foothorir * 1.5 * intensity, - -15.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, + -10.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, ); next.r_foot.ori = Quaternion::rotation_x(-0.8 + footrotr * 0.4 * intensity); next.r_foot.scale = Vec3::one(); - if velocity > 0.1 { + if velocity > 0.01 { next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; next.torso.ori = Quaternion::rotation_x(velocity * -0.05); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; @@ -123,7 +123,7 @@ impl Animation for SwimWieldAnimation { ); next.chest.ori = - Quaternion::rotation_y(u_slowalt * 0.04) * Quaternion::rotation_z(0.15); + Quaternion::rotation_y(u_slowalt * 0.04) * Quaternion::rotation_z(0.25); next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); next.belt.ori = Quaternion::rotation_y(u_slowalt * 0.03) * Quaternion::rotation_z(0.22); diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 68c0861f2b..f825ac585b 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -584,7 +584,7 @@ impl FigureMgr { physics.in_fluid, // In water ) { // Standing - (true, false, _) => anim::character::StandAnimation::update_skeleton( + (true, false, false) => anim::character::StandAnimation::update_skeleton( &CharacterSkeleton::new(), ( active_tool_kind.clone(), From 37c733a06760e23abcfa0d5515ccaf6b527f7caa Mon Sep 17 00:00:00 2001 From: jshipsey Date: Sat, 8 Aug 2020 17:04:50 -0400 Subject: [PATCH 65/71] clippy, consdensed climb, corrected some swim bugs --- voxygen/src/anim/src/character/climb.rs | 302 ++++++++---------------- voxygen/src/anim/src/character/sneak.rs | 5 +- voxygen/src/anim/src/character/swim.rs | 14 +- 3 files changed, 102 insertions(+), 219 deletions(-) diff --git a/voxygen/src/anim/src/character/climb.rs b/voxygen/src/anim/src/character/climb.rs index adcb48b5c4..aee0be362b 100644 --- a/voxygen/src/anim/src/character/climb.rs +++ b/voxygen/src/anim/src/character/climb.rs @@ -28,7 +28,7 @@ impl Animation for ClimbAnimation { skeleton_attr: &SkeletonAttr, ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - + let lateral = Vec2::::from(velocity).magnitude(); let speed = velocity.z; *rate = speed; let constant = 1.0; @@ -60,7 +60,8 @@ impl Animation for ClimbAnimation { .sin() * 0.15, ); - if speed > 0.7 { + let stagnant = if speed > -0.7 { 1.0 } else { 0.0 }; //sets static position when there is no movement + if speed > 0.7 || lateral > 0.1 { next.head.offset = Vec3::new( 0.0, -4.0 + skeleton_attr.head.0, @@ -203,232 +204,113 @@ impl Animation for ClimbAnimation { next.torso.offset = Vec3::new(0.0, -0.2 + smooth * -0.08, 0.4) * skeleton_attr.scaler; next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; - - next.control.scale = Vec3::one(); - - next.l_control.scale = Vec3::one(); - - next.r_control.scale = Vec3::one(); } else { - if speed > -0.7 { - next.head.offset = - Vec3::new(0.0, -2.0 + skeleton_attr.head.0, skeleton_attr.head.1); - next.head.ori = Quaternion::rotation_x(2.0 * head_look.y.abs()) - * Quaternion::rotation_z(3.5 * head_look.x.abs()); - next.head.scale = Vec3::one() * skeleton_attr.head_scale; + next.head.offset = Vec3::new( + 0.0, + -1.0 - stagnant + skeleton_attr.head.0, + skeleton_attr.head.1, + ); + next.head.ori = Quaternion::rotation_x( + -0.25 * (1.0 - stagnant) + stagnant * 2.0 * head_look.x.abs(), + ) * Quaternion::rotation_z(stagnant * 3.5 * head_look.x.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; - next.chest.offset = Vec3::new( - 0.0, - -2.0 + skeleton_attr.chest.0 + 3.0, - skeleton_attr.chest.1, - ); - next.chest.ori = Quaternion::rotation_z(0.0) - * Quaternion::rotation_x(0.3) - * Quaternion::rotation_z(0.6); - next.chest.scale = Vec3::one(); + next.chest.offset = Vec3::new(0.0, 1.0 + skeleton_attr.chest.0, skeleton_attr.chest.1); + next.chest.ori = Quaternion::rotation_z(0.6 * stagnant) + * Quaternion::rotation_x((0.2 + drop * 0.05) * (1.0 - stagnant)); + next.chest.scale = Vec3::one(); - next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); - next.belt.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.1); - next.belt.scale = Vec3::one(); + next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); + next.belt.ori = Quaternion::rotation_x(0.1 + dropa * 0.1); + next.belt.scale = Vec3::one(); - next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.ori = Quaternion::rotation_x(-0.2); - next.back.scale = Vec3::one() * 1.02; + next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.ori = Quaternion::rotation_x( + -0.2 + dropa * 0.1 - 0.15 * (1.0 - stagnant) + stagnant * 0.1, + ); + next.back.scale = Vec3::one() * 1.02; - next.shorts.offset = - Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_z(0.0) - * Quaternion::rotation_x(0.1) - * Quaternion::rotation_y(0.0); - next.shorts.scale = Vec3::one(); + next.shorts.offset = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.ori = Quaternion::rotation_x(0.1 + dropa * 0.12 * (1.0 - stagnant)); + next.shorts.scale = Vec3::one(); - next.l_hand.offset = Vec3::new( - -skeleton_attr.hand.0, - 2.0 + skeleton_attr.hand.1, - skeleton_attr.hand.2, - ); - next.l_hand.ori = Quaternion::rotation_x(0.8); - next.l_hand.scale = Vec3::one(); + next.l_hand.offset = Vec3::new( + -skeleton_attr.hand.0, + 7.5 + stagnant * -5.0 + skeleton_attr.hand.1, + 7.0 + stagnant * -7.0 + skeleton_attr.hand.2 + dropa * -1.0 * (1.0 - stagnant), + ); + next.l_hand.ori = Quaternion::rotation_x(2.2 + stagnant * -1.4) + * Quaternion::rotation_y((0.3 + dropa * 0.1) * (1.0 - stagnant)); + next.l_hand.scale = Vec3::one(); - next.r_hand.offset = Vec3::new( - skeleton_attr.hand.0, - 5.5 + skeleton_attr.hand.1, - 5.0 + skeleton_attr.hand.2, - ); - next.r_hand.ori = Quaternion::rotation_x(2.2) * Quaternion::rotation_y(-0.5); - next.r_hand.scale = Vec3::one(); + next.r_hand.offset = Vec3::new( + skeleton_attr.hand.0, + 7.5 + stagnant * -2.5 + skeleton_attr.hand.1, + 5.0 + skeleton_attr.hand.2 + drop * -1.0 * (1.0 - stagnant), + ); + next.r_hand.ori = Quaternion::rotation_x(2.2) + * Quaternion::rotation_y(-0.3 + drop * 0.1 * (1.0 - stagnant)); + next.r_hand.scale = Vec3::one(); - next.l_foot.offset = Vec3::new( - -skeleton_attr.foot.0, - 5.0 + skeleton_attr.foot.1, - 1.0 + skeleton_attr.foot.2, - ); - next.l_foot.ori = Quaternion::rotation_x(0.55); - next.l_foot.scale = Vec3::one(); + next.l_foot.offset = Vec3::new( + -skeleton_attr.foot.0, + 4.0 + stagnant * 3.0 + skeleton_attr.foot.1, + 1.0 + skeleton_attr.foot.2 + drop * -2.0 * (1.0 - stagnant), + ); + next.l_foot.ori = Quaternion::rotation_x(0.55 + drop * 0.1 * (1.0 - stagnant)); + next.l_foot.scale = Vec3::one(); - next.r_foot.offset = Vec3::new( - skeleton_attr.foot.0, - 5.0 + skeleton_attr.foot.1, - -2.0 + skeleton_attr.foot.2, - ); - next.r_foot.ori = Quaternion::rotation_x(0.2); - next.r_foot.scale = Vec3::one(); + next.r_foot.offset = Vec3::new( + skeleton_attr.foot.0, + 2.0 + stagnant * 4.0 + skeleton_attr.foot.1, + -2.0 + skeleton_attr.foot.2 + smooth * 1.0 * (1.0 - stagnant), + ); + next.r_foot.ori = Quaternion::rotation_x(0.2 + smooth * 0.15 * (1.0 - stagnant)); + next.r_foot.scale = Vec3::one(); - next.l_shoulder.offset = Vec3::new( - -skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.l_shoulder.ori = Quaternion::rotation_x(0.0); - next.l_shoulder.scale = Vec3::one() * 1.1; + next.l_shoulder.offset = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.ori = Quaternion::rotation_x(0.0); + next.l_shoulder.scale = Vec3::one() * 1.1; - next.r_shoulder.offset = Vec3::new( - skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.r_shoulder.ori = Quaternion::rotation_x(0.0); - next.r_shoulder.scale = Vec3::one() * 1.1; + next.r_shoulder.offset = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.ori = Quaternion::rotation_x(0.0); + next.r_shoulder.scale = Vec3::one() * 1.1; - next.glider.offset = Vec3::new(0.0, 0.0, 10.0); - next.glider.scale = Vec3::one() * 0.0; + next.glider.offset = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; - next.main.offset = Vec3::new(-7.0, -5.0, 18.0); - next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - next.main.scale = Vec3::one(); + next.main.offset = Vec3::new(-7.0, -5.0, 18.0); + next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); - next.second.offset = Vec3::new(0.0, 0.0, 0.0); - next.second.ori = Quaternion::rotation_y(0.0); - next.second.scale = Vec3::one() * 0.0; + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; - next.lantern.offset = Vec3::new( - skeleton_attr.lantern.0, - skeleton_attr.lantern.1, - skeleton_attr.lantern.2, - ); - next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.lantern.scale = Vec3::one() * 0.65; + next.lantern.offset = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.ori = Quaternion::rotation_x(0.0); + next.lantern.scale = Vec3::one() * 0.65; - next.torso.offset = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; - next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; - - next.control.scale = Vec3::one(); - - next.l_control.scale = Vec3::one(); - - next.r_control.scale = Vec3::one(); - } else { - next.head.offset = - Vec3::new(0.0, -1.0 + skeleton_attr.head.0, skeleton_attr.head.1); - next.head.ori = Quaternion::rotation_x(-0.25) * Quaternion::rotation_z(0.0); - next.head.scale = Vec3::one() * skeleton_attr.head_scale; - - next.chest.offset = Vec3::new( - 0.0, - -2.0 + skeleton_attr.chest.0 + 3.0, - skeleton_attr.chest.1, - ); - next.chest.ori = Quaternion::rotation_z(0.0) - * Quaternion::rotation_x(0.2 + drop * 0.05) - * Quaternion::rotation_z(0.0); - next.chest.scale = Vec3::one(); - - next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); - next.belt.ori = - Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.1 + dropa * 0.1); - next.belt.scale = Vec3::one(); - - next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.ori = Quaternion::rotation_x(-0.15); - next.back.scale = Vec3::one() * 1.02; - - next.shorts.offset = - Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_z(0.0) - * Quaternion::rotation_x(0.1 + dropa * 0.12) - * Quaternion::rotation_y(0.0); - next.shorts.scale = Vec3::one(); - - next.l_hand.offset = Vec3::new( - -skeleton_attr.hand.0, - 7.5 + skeleton_attr.hand.1, - 7.0 + skeleton_attr.hand.2 + dropa * -1.0, - ); - next.l_hand.ori = - Quaternion::rotation_x(2.2) * Quaternion::rotation_y(0.3 + dropa * 0.1); - next.l_hand.scale = Vec3::one(); - - next.r_hand.offset = Vec3::new( - skeleton_attr.hand.0, - 7.5 + skeleton_attr.hand.1, - 5.0 + skeleton_attr.hand.2 + drop * -1.0, - ); - next.r_hand.ori = - Quaternion::rotation_x(2.2) * Quaternion::rotation_y(-0.3 + drop * 0.1); - next.r_hand.scale = Vec3::one(); - - next.l_foot.offset = Vec3::new( - -skeleton_attr.foot.0, - 4.0 + skeleton_attr.foot.1, - 1.0 + skeleton_attr.foot.2 + drop * -2.0, - ); - next.l_foot.ori = Quaternion::rotation_x(0.55 + drop * 0.1); - next.l_foot.scale = Vec3::one(); - - next.r_foot.offset = Vec3::new( - skeleton_attr.foot.0, - 2.0 + skeleton_attr.foot.1, - -2.0 + skeleton_attr.foot.2 + smooth * 1.0, - ); - next.r_foot.ori = Quaternion::rotation_x(0.2 + smooth * 0.15); - next.r_foot.scale = Vec3::one(); - - next.l_shoulder.offset = Vec3::new( - -skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.l_shoulder.ori = Quaternion::rotation_x(0.0); - next.l_shoulder.scale = Vec3::one() * 1.1; - - next.r_shoulder.offset = Vec3::new( - skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.r_shoulder.ori = Quaternion::rotation_x(0.0); - next.r_shoulder.scale = Vec3::one() * 1.1; - - next.glider.offset = Vec3::new(0.0, 0.0, 10.0); - next.glider.scale = Vec3::one() * 0.0; - - next.main.offset = Vec3::new(-7.0, -5.0, 18.0); - next.main.ori = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - next.main.scale = Vec3::one(); - - next.second.offset = Vec3::new(0.0, 0.0, 0.0); - next.second.ori = Quaternion::rotation_y(0.0); - next.second.scale = Vec3::one() * 0.0; - - next.lantern.offset = Vec3::new( - skeleton_attr.lantern.0, - skeleton_attr.lantern.1, - skeleton_attr.lantern.2, - ); - next.lantern.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.lantern.scale = Vec3::one() * 0.65; - - next.torso.offset = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; - next.torso.ori = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; - - next.control.scale = Vec3::one(); - - next.l_control.scale = Vec3::one(); - } + next.torso.offset = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; + next.torso.ori = Quaternion::rotation_x(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; }; + next.control.scale = Vec3::one(); next.r_control.scale = Vec3::one(); + next.l_control.scale = Vec3::one(); next.second.scale = match ( active_tool_kind.map(|tk| tk.hands()), diff --git a/voxygen/src/anim/src/character/sneak.rs b/voxygen/src/anim/src/character/sneak.rs index 281607f259..52563090a1 100644 --- a/voxygen/src/anim/src/character/sneak.rs +++ b/voxygen/src/anim/src/character/sneak.rs @@ -13,7 +13,6 @@ impl Animation for SneakAnimation { const UPDATE_FN: &'static [u8] = b"character_sneak\0"; #[cfg_attr(feature = "be-dyn-lib", export_name = "character_sneak")] - #[allow(clippy::identity_conversion)] // TODO: Pending review in #587 fn update_skeleton_inner( skeleton: &Self::Skeleton, @@ -75,10 +74,10 @@ impl Animation for SneakAnimation { * 0.1, ); - let ori = Vec2::from(orientation); + let ori: Vec2 = Vec2::from(orientation); let last_ori = Vec2::from(last_ori); let tilt = if Vec2::new(ori, last_ori) - .map(|o| Vec2::::from(o).magnitude_squared()) + .map(|o| o.magnitude_squared()) .map(|m| m > 0.001 && m.is_finite()) .reduce_and() && ori.angle_between(last_ori).is_finite() diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index 939eda7fd4..b0c2c33d32 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -33,9 +33,10 @@ impl Animation for SwimAnimation { ) -> Self::Skeleton { let mut next = (*skeleton).clone(); let avgspeed = Vec2::::from(avg_vel).magnitude(); - let avgtotal = Vec3::::from(avg_vel).magnitude(); - let speed = Vec3::::from(velocity).magnitude(); + let avgtotal = avg_vel.magnitude(); + + let speed = velocity.magnitude(); *rate = 1.0; let tempo = if speed > 0.5 { 1.5 } else { 0.7 }; let intensity = if speed > 0.5 { 1.0 } else { 0.3 }; @@ -96,12 +97,12 @@ impl Animation for SwimAnimation { skeleton_attr.head.1 - 1.0 + short * 0.3, ); next.head.ori = - Quaternion::rotation_z(head_look.x * 0.5 + short * -0.2 * intensity + tilt * 3.0) + Quaternion::rotation_z(head_look.x * 0.3 + short * -0.2 * intensity + tilt * 3.0) * Quaternion::rotation_x( (0.4 * head_look.y * (1.0 / intensity)).abs() + 0.45 * intensity + velocity.z * 0.03 - - (abstilt * 1.8), + - (abstilt * 1.8).min(0.0), ); next.head.scale = Vec3::one() * skeleton_attr.head_scale; @@ -230,11 +231,12 @@ impl Animation for SwimAnimation { } else { avgtotal }; - next.torso.offset = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; + next.torso.offset = Vec3::new(0.0, 0.0, 1.0 - avgspeed * 0.05) * skeleton_attr.scaler; next.torso.ori = Quaternion::rotation_x( (((1.0 / switch) * PI / 2.0 + avg_vel.z * 0.12).min(1.57) - PI / 2.0) + avgspeed * avg_vel.z * -0.003, - ) * Quaternion::rotation_z(tilt * 8.0); + ) * Quaternion::rotation_y(tilt * 8.0) + * Quaternion::rotation_z(tilt * 8.0); next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; next.control.scale = Vec3::one(); //avgspeed*-0.14*reverse + From 45971fa4033e351900ff851aa10db86ca7d07b3b Mon Sep 17 00:00:00 2001 From: jshipsey Date: Sat, 8 Aug 2020 17:05:48 -0400 Subject: [PATCH 66/71] state corrections --- common/src/states/climb.rs | 3 ++- common/src/states/glide.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 44642ec656..4bf3cf71b2 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -87,7 +87,8 @@ impl CharacterBehavior for Data { }, Climb::Hold => { // Antigrav - update.vel.0.z = (update.vel.0.z + data.dt.0 * GRAVITY * 1.1).min(CLIMB_SPEED); + update.vel.0.z = + (update.vel.0.z + data.dt.0 * GRAVITY * 1.075).min(CLIMB_SPEED); update.vel.0 = Lerp::lerp( update.vel.0, Vec3::zero(), diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index c8c06b68be..93449a2ad3 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -24,7 +24,9 @@ impl CharacterBehavior for Data { update.character = CharacterState::GlideWield; return update; } - + if data.physics.in_fluid { + update.character = CharacterState::Idle; + } // If there is a wall in front of character and they are trying to climb go to // climb handle_climb(&data, &mut update); From bc1fabc41b3dd0beee7902dfac13a993318e53e3 Mon Sep 17 00:00:00 2001 From: nepo Date: Sat, 8 Aug 2020 22:37:50 +0000 Subject: [PATCH 67/71] nepo/new bones --- .../voxel/biped_large_center_manifest.ron | 120 ++++++++++++++++++ voxygen/src/anim/Cargo.toml | 2 +- voxygen/src/anim/src/biped_large/idle.rs | 19 +++ voxygen/src/anim/src/biped_large/jump.rs | 8 ++ voxygen/src/anim/src/biped_large/mod.rs | 39 +++++- voxygen/src/anim/src/biped_large/run.rs | 13 ++ voxygen/src/anim/src/biped_large/wield.rs | 13 ++ voxygen/src/anim/src/character/swim.rs | 3 - voxygen/src/scene/figure/cache.rs | 18 ++- voxygen/src/scene/figure/load.rs | 66 ++++++++++ 10 files changed, 289 insertions(+), 12 deletions(-) diff --git a/assets/voxygen/voxel/biped_large_center_manifest.ron b/assets/voxygen/voxel/biped_large_center_manifest.ron index 55fc0ecc34..c1a154eab9 100644 --- a/assets/voxygen/voxel/biped_large_center_manifest.ron +++ b/assets/voxygen/voxel/biped_large_center_manifest.ron @@ -12,6 +12,18 @@ offset: (-5.0, -4.5, -9.0), center: ("npc.ogre.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -30,6 +42,18 @@ offset: (-5.0, -4.5, -9.0), center: ("npc.ogre.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -48,6 +72,18 @@ offset: (-6.0, -5.5, -12.0), center: ("npc.cyclops.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-5.0, -6.5, -4.0), center: ("npc.cyclops.male.hammer"), @@ -66,6 +102,18 @@ offset: (-6.0, -5.5, -12.0), center: ("npc.cyclops.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-5.0, -6.5, -4.0), center: ("npc.cyclops.male.hammer"), @@ -84,6 +132,18 @@ offset: (-4.0, -2.0, -4.0), center: ("npc.wendigo.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -102,6 +162,18 @@ offset: (-4.0, -2.0, -4.0), center: ("npc.wendigo.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -120,6 +192,18 @@ offset: (-6.0, -3.5, -5.0), center: ("npc.troll.male.torso_lower"), ), + jaw: ( + offset: (-4.0, 0.0, -4.5), + center: ("npc.troll.male.jaw"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -138,6 +222,18 @@ offset: (-6.0, -3.5, -5.0), center: ("npc.troll.male.torso_lower"), ), + jaw: ( + offset: (-4.0, 0.0, -4.5), + center: ("npc.troll.male.jaw"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-8.0, -4.5, -5.0), center: ("armor.empty"), @@ -158,6 +254,18 @@ offset: (-8.0, -6.0, -9.0), center: ("npc.dullahan.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-1.5, -9.0, -10.0), center: ("npc.dullahan.male.sword"), @@ -177,6 +285,18 @@ offset: (-8.0, -6.0, -9.0), center: ("npc.dullahan.male.torso_lower"), ), + jaw: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + tail: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), + second: ( + offset: (0.0, 0.0, 0.0), + center: ("armor.empty"), + ), main: ( offset: (-1.5, -9.0, -10.0), center: ("npc.dullahan.male.sword"), diff --git a/voxygen/src/anim/Cargo.toml b/voxygen/src/anim/Cargo.toml index ab4d0e2bfe..d54f1519c9 100644 --- a/voxygen/src/anim/Cargo.toml +++ b/voxygen/src/anim/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" name = "voxygen_anim" # Uncomment to use animation hot reloading # Note: this breaks `cargo test` -# crate-type = ["lib", "cdylib"] +#crate-type = ["lib", "cdylib"] [features] use-dyn-lib = ["libloading", "notify", "lazy_static", "tracing", "find_folder"] diff --git a/voxygen/src/anim/src/biped_large/idle.rs b/voxygen/src/anim/src/biped_large/idle.rs index d240811ace..8e9ca72cbc 100644 --- a/voxygen/src/anim/src/biped_large/idle.rs +++ b/voxygen/src/anim/src/biped_large/idle.rs @@ -37,6 +37,8 @@ impl Animation for IdleAnimation { * 0.25, ); + let wave_slow = (anim_time as f32 * 0.8).sin(); + next.head.offset = Vec3::new( 0.0, skeleton_attr.head.0, @@ -61,10 +63,27 @@ impl Animation for IdleAnimation { next.lower_torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.offset = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.ori = Quaternion::rotation_x(wave_slow * 0.09); + next.jaw.scale = Vec3::one(); + + next.tail.offset = Vec3::new( + 0.0, + skeleton_attr.tail.0, + skeleton_attr.tail.1 + torso * 0.0, + ); + next.tail.ori = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.control.offset = Vec3::new(0.0, 0.0, 0.0); next.control.ori = Quaternion::rotation_z(0.0); next.control.scale = Vec3::one(); + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.main.offset = Vec3::new(-5.0, -7.0, 7.0); next.main.ori = Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.6) * Quaternion::rotation_z(1.57); diff --git a/voxygen/src/anim/src/biped_large/jump.rs b/voxygen/src/anim/src/biped_large/jump.rs index 650db18ffb..b2d9c28e90 100644 --- a/voxygen/src/anim/src/biped_large/jump.rs +++ b/voxygen/src/anim/src/biped_large/jump.rs @@ -41,6 +41,14 @@ impl Animation for JumpAnimation { next.lower_torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.offset = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.ori = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.offset = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1 * 0.0); + next.tail.ori = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.shoulder_l.offset = Vec3::new( -skeleton_attr.shoulder.0, skeleton_attr.shoulder.1, diff --git a/voxygen/src/anim/src/biped_large/mod.rs b/voxygen/src/anim/src/biped_large/mod.rs index 6af323397f..3f744a3589 100644 --- a/voxygen/src/anim/src/biped_large/mod.rs +++ b/voxygen/src/anim/src/biped_large/mod.rs @@ -15,9 +15,12 @@ use vek::Vec3; #[derive(Clone, Default)] pub struct BipedLargeSkeleton { head: Bone, + jaw: Bone, upper_torso: Bone, lower_torso: Bone, + tail: Bone, main: Bone, + second: Bone, shoulder_l: Bone, shoulder_r: Bone, hand_l: Bone, @@ -40,13 +43,16 @@ impl Skeleton for BipedLargeSkeleton { #[cfg(feature = "use-dyn-lib")] const COMPUTE_FN: &'static [u8] = b"biped_large_compute_mats\0"; - fn bone_count(&self) -> usize { 12 } + fn bone_count(&self) -> usize { 15 } #[cfg_attr(feature = "be-dyn-lib", export_name = "biped_large_compute_mats")] fn compute_matrices_inner(&self) -> ([FigureBoneData; 16], Vec3) { + let jaw_mat = self.jaw.compute_base_matrix(); let upper_torso_mat = self.upper_torso.compute_base_matrix(); let lower_torso_mat = self.lower_torso.compute_base_matrix(); + let tail_mat = self.tail.compute_base_matrix(); let main_mat = self.main.compute_base_matrix(); + let second_mat = self.second.compute_base_matrix(); let shoulder_l_mat = self.shoulder_l.compute_base_matrix(); let shoulder_r_mat = self.shoulder_r.compute_base_matrix(); let hand_l_mat = self.hand_l.compute_base_matrix(); @@ -59,9 +65,14 @@ impl Skeleton for BipedLargeSkeleton { ( [ FigureBoneData::new(torso_mat * upper_torso_mat * self.head.compute_base_matrix()), + FigureBoneData::new( + torso_mat * upper_torso_mat * self.head.compute_base_matrix() * jaw_mat, + ), FigureBoneData::new(torso_mat * upper_torso_mat), FigureBoneData::new(torso_mat * upper_torso_mat * lower_torso_mat), + FigureBoneData::new(torso_mat * upper_torso_mat * lower_torso_mat * tail_mat), FigureBoneData::new(torso_mat * control_mat * upper_torso_mat * main_mat), + FigureBoneData::new(torso_mat * control_mat * upper_torso_mat * second_mat), FigureBoneData::new(torso_mat * upper_torso_mat * shoulder_l_mat), FigureBoneData::new(torso_mat * upper_torso_mat * shoulder_r_mat), FigureBoneData::new(torso_mat * control_mat * upper_torso_mat * hand_l_mat), @@ -71,9 +82,6 @@ impl Skeleton for BipedLargeSkeleton { FigureBoneData::new(self.foot_l.compute_base_matrix()), FigureBoneData::new(self.foot_r.compute_base_matrix()), FigureBoneData::default(), - FigureBoneData::default(), - FigureBoneData::default(), - FigureBoneData::default(), ], Vec3::default(), ) @@ -81,9 +89,12 @@ impl Skeleton for BipedLargeSkeleton { fn interpolate(&mut self, target: &Self, dt: f32) { self.head.interpolate(&target.head, dt); + self.jaw.interpolate(&target.jaw, dt); self.upper_torso.interpolate(&target.upper_torso, dt); self.lower_torso.interpolate(&target.lower_torso, dt); + self.tail.interpolate(&target.tail, dt); self.main.interpolate(&target.main, dt); + self.second.interpolate(&target.second, dt); self.shoulder_l.interpolate(&target.shoulder_l, dt); self.shoulder_r.interpolate(&target.shoulder_r, dt); self.hand_l.interpolate(&target.hand_l, dt); @@ -99,8 +110,10 @@ impl Skeleton for BipedLargeSkeleton { pub struct SkeletonAttr { head: (f32, f32), + jaw: (f32, f32), upper_torso: (f32, f32), lower_torso: (f32, f32), + tail: (f32, f32), shoulder: (f32, f32, f32), hand: (f32, f32, f32), leg: (f32, f32, f32), @@ -122,8 +135,10 @@ impl Default for SkeletonAttr { fn default() -> Self { Self { head: (0.0, 0.0), + jaw: (0.0, 0.0), upper_torso: (0.0, 0.0), lower_torso: (0.0, 0.0), + tail: (0.0, 0.0), shoulder: (0.0, 0.0, 0.0), hand: (0.0, 0.0, 0.0), leg: (0.0, 0.0, 0.0), @@ -143,6 +158,13 @@ impl<'a> From<&'a comp::biped_large::Body> for SkeletonAttr { (Troll, _) => (6.0, 10.0), (Dullahan, _) => (3.0, 6.0), }, + jaw: match (body.species, body.body_type) { + (Ogre, _) => (0.0, 0.0), + (Cyclops, _) => (0.0, 0.0), + (Wendigo, _) => (0.0, 0.0), + (Troll, _) => (2.0, -4.0), + (Dullahan, _) => (0.0, 0.0), + }, upper_torso: match (body.species, body.body_type) { (Ogre, _) => (0.0, 19.0), (Cyclops, _) => (-2.0, 27.0), @@ -157,11 +179,18 @@ impl<'a> From<&'a comp::biped_large::Body> for SkeletonAttr { (Troll, _) => (1.0, -10.5), (Dullahan, _) => (0.0, -6.5), }, + tail: match (body.species, body.body_type) { + (Ogre, _) => (0.0, 0.0), + (Cyclops, _) => (0.0, 0.0), + (Wendigo, _) => (0.0, 0.0), + (Troll, _) => (0.0, 0.0), + (Dullahan, _) => (0.0, 0.0), + }, shoulder: match (body.species, body.body_type) { (Ogre, _) => (6.1, 0.5, 2.5), (Cyclops, _) => (9.5, 2.5, 2.5), (Wendigo, _) => (9.0, 0.5, -0.5), - (Troll, _) => (11.0, 0.5, -2.5), + (Troll, _) => (11.0, 0.5, -1.5), (Dullahan, _) => (14.0, 0.5, 4.5), }, hand: match (body.species, body.body_type) { diff --git a/voxygen/src/anim/src/biped_large/run.rs b/voxygen/src/anim/src/biped_large/run.rs index ac33aa2fb9..a7929510f9 100644 --- a/voxygen/src/anim/src/biped_large/run.rs +++ b/voxygen/src/anim/src/biped_large/run.rs @@ -77,6 +77,19 @@ impl Animation for RunAnimation { next.lower_torso.ori = Quaternion::rotation_z(short * 0.15) * Quaternion::rotation_x(0.14); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.offset = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.ori = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.offset = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1 * 0.0); + next.tail.ori = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.control.offset = Vec3::new(0.0, 0.0, 0.0); next.control.ori = Quaternion::rotation_z(0.0); next.control.scale = Vec3::one(); diff --git a/voxygen/src/anim/src/biped_large/wield.rs b/voxygen/src/anim/src/biped_large/wield.rs index 4edd995536..9ab73c16eb 100644 --- a/voxygen/src/anim/src/biped_large/wield.rs +++ b/voxygen/src/anim/src/biped_large/wield.rs @@ -77,6 +77,11 @@ impl Animation for WieldAnimation { * Quaternion::rotation_z(1.0); next.main.scale = Vec3::one() * 1.02; + next.second.offset = Vec3::new(0.0, 0.0, 0.0); + next.second.ori = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.hand_l.offset = Vec3::new( -skeleton_attr.hand.0 - 7.0, skeleton_attr.hand.1 - 7.0, @@ -119,6 +124,14 @@ impl Animation for WieldAnimation { next.lower_torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.offset = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1 * 0.0); + next.jaw.ori = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.offset = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1); + next.tail.ori = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.shoulder_l.offset = Vec3::new( -skeleton_attr.shoulder.0, skeleton_attr.shoulder.1, diff --git a/voxygen/src/anim/src/character/swim.rs b/voxygen/src/anim/src/character/swim.rs index e7e23023ae..3d28c3bdff 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -88,15 +88,12 @@ impl Animation for SwimAnimation { next.chest.scale = Vec3::one(); next.belt.offset = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); - next.belt.ori = Quaternion::rotation_z(short * 0.30); next.belt.scale = Vec3::one(); next.back.offset = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.ori = Quaternion::rotation_z(0.0); next.back.scale = Vec3::one() * 1.02; next.shorts.offset = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); - next.shorts.ori = Quaternion::rotation_z(short * 0.5); next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index 5aadd9efac..98ee3824e3 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -576,6 +576,11 @@ impl FigureModelCache { body.body_type, generate_mesh, )), + Some(biped_large_center_spec.mesh_jaw( + body.species, + body.body_type, + generate_mesh, + )), Some(biped_large_center_spec.mesh_torso_upper( body.species, body.body_type, @@ -586,11 +591,21 @@ impl FigureModelCache { body.body_type, generate_mesh, )), + Some(biped_large_center_spec.mesh_tail( + body.species, + body.body_type, + generate_mesh, + )), Some(biped_large_center_spec.mesh_main( body.species, body.body_type, generate_mesh, )), + Some(biped_large_center_spec.mesh_second( + body.species, + body.body_type, + generate_mesh, + )), Some(biped_large_lateral_spec.mesh_shoulder_l( body.species, body.body_type, @@ -632,9 +647,6 @@ impl FigureModelCache { generate_mesh, )), None, - None, - None, - None, ] }, Body::Golem(body) => { diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 007a4c91ee..aafa1b8970 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -2481,9 +2481,12 @@ pub struct BipedLargeCenterSpec(HashMap<(BLSpecies, BLBodyType), SidedBLCenterVo #[derive(Serialize, Deserialize)] struct SidedBLCenterVoxSpec { head: BipedLargeCenterSubSpec, + jaw: BipedLargeCenterSubSpec, torso_upper: BipedLargeCenterSubSpec, torso_lower: BipedLargeCenterSubSpec, + tail: BipedLargeCenterSubSpec, main: BipedLargeCenterSubSpec, + second: BipedLargeCenterSubSpec, } #[derive(Serialize, Deserialize)] struct BipedLargeCenterSubSpec { @@ -2554,6 +2557,27 @@ impl BipedLargeCenterSpec { generate_mesh(¢er, Vec3::from(spec.head.offset)) } + pub fn mesh_jaw( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(&Segment, Vec3) -> Mesh, + ) -> Mesh { + let spec = match self.0.get(&(species, body_type)) { + Some(spec) => spec, + None => { + error!( + "No jaw specification exists for the combination of {:?} and {:?}", + species, body_type + ); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5), generate_mesh); + }, + }; + let center = graceful_load_segment(&spec.jaw.center.0); + + generate_mesh(¢er, Vec3::from(spec.jaw.offset)) + } + pub fn mesh_torso_upper( &self, species: BLSpecies, @@ -2596,6 +2620,27 @@ impl BipedLargeCenterSpec { generate_mesh(¢er, Vec3::from(spec.torso_lower.offset)) } + pub fn mesh_tail( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(&Segment, Vec3) -> Mesh, + ) -> Mesh { + let spec = match self.0.get(&(species, body_type)) { + Some(spec) => spec, + None => { + error!( + "No tail specification exists for the combination of {:?} and {:?}", + species, body_type + ); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5), generate_mesh); + }, + }; + let center = graceful_load_segment(&spec.tail.center.0); + + generate_mesh(¢er, Vec3::from(spec.tail.offset)) + } + pub fn mesh_main( &self, species: BLSpecies, @@ -2616,6 +2661,27 @@ impl BipedLargeCenterSpec { generate_mesh(¢er, Vec3::from(spec.main.offset)) } + + pub fn mesh_second( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(&Segment, Vec3) -> Mesh, + ) -> Mesh { + let spec = match self.0.get(&(species, body_type)) { + Some(spec) => spec, + None => { + error!( + "No second weapon specification exists for the combination of {:?} and {:?}", + species, body_type + ); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5), generate_mesh); + }, + }; + let center = graceful_load_segment(&spec.second.center.0); + + generate_mesh(¢er, Vec3::from(spec.second.offset)) + } } impl BipedLargeLateralSpec { pub fn load_watched(indicator: &mut ReloadIndicator) -> Arc { From 6a9b089ad523e87e80c91a2f099ecdd71c6b2adb Mon Sep 17 00:00:00 2001 From: Songtronix Date: Sun, 9 Aug 2020 08:05:15 +0200 Subject: [PATCH 68/71] change: allow `test-server` alias to run in workspace root --- .cargo/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config b/.cargo/config index 51f84ab9e9..77c060848a 100644 --- a/.cargo/config +++ b/.cargo/config @@ -5,6 +5,6 @@ rustflags = [ [alias] generate = "run --package tools --" -test-server = "run --bin veloren-server-cli --no-default-features" +test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features" server = "run --bin veloren-server-cli" From 6b707a1ab7ea8a77196bf208b810dd88fdcc43bf Mon Sep 17 00:00:00 2001 From: IBotDEU Date: Sun, 9 Aug 2020 20:30:22 +0000 Subject: [PATCH 69/71] implemented ability to select bit depth and refresh rate and implemented seperate setting for fullscreen resolution --- CHANGELOG.md | 1 + Cargo.lock | 1 + assets/voxygen/i18n/de_DE.ron | 5 + assets/voxygen/i18n/en.ron | 4 + voxygen/Cargo.toml | 1 + voxygen/src/hud/mod.rs | 12 ++ voxygen/src/hud/settings_window.rs | 175 ++++++++++++++++++++++- voxygen/src/session.rs | 31 +++++ voxygen/src/settings.rs | 6 + voxygen/src/window.rs | 217 ++++++++++++++++++++++++++--- 10 files changed, 433 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078d5790e9..06d7096ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add detection of entities under the cursor - Functional group-system with exp-sharing and disabled damage to group members - Some Campfire, fireball & bomb; particle, light & sound effects. +- Added setting to change resolution ### Changed diff --git a/Cargo.lock b/Cargo.lock index c748709104..afe3d80245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4719,6 +4719,7 @@ dependencies = [ "guillotiere", "hashbrown", "image", + "itertools", "msgbox", "num 0.2.1", "old_school_gfx_glutin_ext", diff --git a/assets/voxygen/i18n/de_DE.ron b/assets/voxygen/i18n/de_DE.ron index f1f3f9da17..86f73364f8 100644 --- a/assets/voxygen/i18n/de_DE.ron +++ b/assets/voxygen/i18n/de_DE.ron @@ -70,6 +70,7 @@ VoxygenLocalization( "common.fatal_error": "Fataler Fehler", "common.decline": "Ablehnen", "common.you": "Ihr", + "common.automatic": "Auto", /// End Common section // Message when connection to the server is lost @@ -297,6 +298,10 @@ magischen Gegenstände ergattern?"#, "hud.settings.fluid_rendering_mode.cheap": "Niedrig", "hud.settings.fluid_rendering_mode.shiny": "Hoch", "hud.settings.cloud_rendering_mode.regular": "Realistisch", + "hud.settings.particles": "Partikel", + "hud.settings.resolution": "Auflösung", + "hud.settings.bit_depth": "Bittiefe", + "hud.settings.refresh_rate": "Bildwiederholrate", "hud.settings.fullscreen": "Vollbild", "hud.settings.save_window_size": "Größe speichern", diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index b3a2004539..a9272a153e 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -72,6 +72,7 @@ VoxygenLocalization( "common.error": "Error", "common.fatal_error": "Fatal Error", "common.you": "You", + "common.automatic": "Auto", // Message when connection to the server is lost "common.connection_lost": r#"Connection lost! @@ -298,6 +299,9 @@ magically infused items?"#, "hud.settings.fluid_rendering_mode.shiny": "Shiny", "hud.settings.cloud_rendering_mode.regular": "Regular", "hud.settings.particles": "Particles", + "hud.settings.resolution": "Resolution", + "hud.settings.bit_depth": "Bit Depth", + "hud.settings.refresh_rate": "Refresh Rate", "hud.settings.fullscreen": "Fullscreen", "hud.settings.save_window_size": "Save window size", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index e8ff69392e..937e894ffb 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -69,6 +69,7 @@ bincode = "1.2" deunicode = "1.0" uvth = "3.1.1" const-tweaker = { version = "0.3.1", optional = true } +itertools = "0.9.0" # Logging tracing = "0.1" diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 20a48df35e..440bb77848 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -283,6 +283,9 @@ pub enum Event { ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), ChangeFluidMode(FluidMode), + ChangeResolution([u16; 2]), + ChangeBitDepth(Option), + ChangeRefreshRate(Option), CrosshairTransp(f32), ChatTransp(f32), ChatCharName(bool), @@ -1908,6 +1911,15 @@ impl Hud { settings_window::Event::ChangeFluidMode(new_fluid_mode) => { events.push(Event::ChangeFluidMode(new_fluid_mode)); }, + settings_window::Event::ChangeResolution(new_resolution) => { + events.push(Event::ChangeResolution(new_resolution)); + }, + settings_window::Event::ChangeBitDepth(new_bit_depth) => { + events.push(Event::ChangeBitDepth(new_bit_depth)); + }, + settings_window::Event::ChangeRefreshRate(new_refresh_rate) => { + events.push(Event::ChangeRefreshRate(new_refresh_rate)); + }, settings_window::Event::ChangeLanguage(language) => { events.push(Event::ChangeLanguage(language)); }, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index f9e6b7d17b..7ece673e69 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -17,6 +17,10 @@ use conrod_core::{ WidgetCommon, }; +use itertools::Itertools; +use std::iter::once; +use winit::monitor::VideoMode; + const FPS_CHOICES: [u32; 11] = [15, 30, 40, 50, 60, 90, 120, 144, 240, 300, 500]; widget_ids! { @@ -111,8 +115,17 @@ widget_ids! { cloud_mode_list, fluid_mode_text, fluid_mode_list, + // + resolution, + resolution_label, + bit_depth, + bit_depth_label, + refresh_rate, + refresh_rate_label, + // particles_button, particles_label, + // fullscreen_button, fullscreen_label, save_window_size_button, @@ -239,6 +252,9 @@ pub enum Event { ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), ChangeFluidMode(FluidMode), + ChangeResolution([u16; 2]), + ChangeBitDepth(Option), + ChangeRefreshRate(Option), AdjustMusicVolume(f32), AdjustSfxVolume(f32), ChangeAudioDevice(String), @@ -2039,11 +2055,168 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ToggleParticlesEnabled(particles_enabled)); } + // Resolution, Bit Depth and Refresh Rate + let video_modes: Vec = self + .global_state + .window + .window() + .window() + .current_monitor() + .video_modes() + .collect(); + + // Resolution + let resolutions: Vec<[u16; 2]> = video_modes + .iter() + .sorted_by_key(|mode| mode.size().height) + .sorted_by_key(|mode| mode.size().width) + .map(|mode| [mode.size().width as u16, mode.size().height as u16]) + .dedup() + .collect(); + + Text::new(&self.localized_strings.get("hud.settings.resolution")) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.particles_label, 8.0) + .color(TEXT_COLOR) + .set(state.ids.resolution_label, ui); + + if let Some(clicked) = DropDownList::new( + resolutions + .iter() + .map(|res| format!("{}x{}", res[0], res[1])) + .collect::>() + .as_slice(), + resolutions + .iter() + .position(|res| res == &self.global_state.settings.graphics.resolution), + ) + .w_h(128.0, 22.0) + .color(MENU_BG) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.opensans.conrod_id) + .down_from(state.ids.resolution_label, 10.0) + .set(state.ids.resolution, ui) + { + events.push(Event::ChangeResolution(resolutions[clicked])); + } + + // Bit Depth and Refresh Rate + let correct_res: Vec = video_modes + .into_iter() + .filter(|mode| { + mode.size().width == self.global_state.settings.graphics.resolution[0] as u32 + }) + .filter(|mode| { + mode.size().height == self.global_state.settings.graphics.resolution[1] as u32 + }) + .collect(); + + // Bit Depth + let bit_depths: Vec = correct_res + .iter() + .filter( + |mode| match self.global_state.settings.graphics.refresh_rate { + Some(refresh_rate) => mode.refresh_rate() == refresh_rate, + None => true, + }, + ) + .sorted_by_key(|mode| mode.bit_depth()) + .map(|mode| mode.bit_depth()) + .rev() + .dedup() + .collect(); + + Text::new(&self.localized_strings.get("hud.settings.bit_depth")) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.particles_label, 8.0) + .right_from(state.ids.resolution, 8.0) + .color(TEXT_COLOR) + .set(state.ids.bit_depth_label, ui); + + if let Some(clicked) = DropDownList::new( + once(String::from(self.localized_strings.get("common.automatic"))) + .chain(bit_depths.iter().map(|depth| format!("{}", depth))) + .collect::>() + .as_slice(), + match self.global_state.settings.graphics.bit_depth { + Some(bit_depth) => bit_depths + .iter() + .position(|depth| depth == &bit_depth) + .map(|index| index + 1), + None => Some(0), + }, + ) + .w_h(128.0, 22.0) + .color(MENU_BG) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.opensans.conrod_id) + .down_from(state.ids.bit_depth_label, 10.0) + .right_from(state.ids.resolution, 8.0) + .set(state.ids.bit_depth, ui) + { + events.push(Event::ChangeBitDepth(if clicked == 0 { + None + } else { + Some(bit_depths[clicked - 1]) + })); + } + + // Refresh Rate + let refresh_rates: Vec = correct_res + .into_iter() + .filter(|mode| match self.global_state.settings.graphics.bit_depth { + Some(bit_depth) => mode.bit_depth() == bit_depth, + None => true, + }) + .sorted_by_key(|mode| mode.refresh_rate()) + .map(|mode| mode.refresh_rate()) + .rev() + .dedup() + .collect(); + + Text::new(&self.localized_strings.get("hud.settings.refresh_rate")) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.particles_label, 8.0) + .right_from(state.ids.bit_depth, 8.0) + .color(TEXT_COLOR) + .set(state.ids.refresh_rate_label, ui); + + if let Some(clicked) = DropDownList::new( + once(String::from(self.localized_strings.get("common.automatic"))) + .chain(refresh_rates.iter().map(|rate| format!("{}", rate))) + .collect::>() + .as_slice(), + match self.global_state.settings.graphics.refresh_rate { + Some(refresh_rate) => refresh_rates + .iter() + .position(|rate| rate == &refresh_rate) + .map(|index| index + 1), + None => Some(0), + }, + ) + .w_h(128.0, 22.0) + .color(MENU_BG) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.opensans.conrod_id) + .down_from(state.ids.refresh_rate_label, 10.0) + .right_from(state.ids.bit_depth, 8.0) + .set(state.ids.refresh_rate, ui) + { + events.push(Event::ChangeRefreshRate(if clicked == 0 { + None + } else { + Some(refresh_rates[clicked - 1]) + })); + } + // Fullscreen Text::new(&self.localized_strings.get("hud.settings.fullscreen")) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) - .down_from(state.ids.particles_label, 8.0) + .down_from(state.ids.resolution, 8.0) .color(TEXT_COLOR) .set(state.ids.fullscreen_label, ui); diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 0decee01f3..dc43964b3d 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -949,6 +949,37 @@ impl PlayState for SessionState { global_state.settings.graphics.fluid_mode = new_fluid_mode; global_state.settings.save_to_file_warn(); }, + HudEvent::ChangeResolution(new_resolution) => { + // Do this first so if it crashes the setting isn't saved :) + global_state.window.fullscreen( + global_state.settings.graphics.fullscreen, + new_resolution, + global_state.settings.graphics.bit_depth, + global_state.settings.graphics.refresh_rate, + ); + global_state.settings.graphics.resolution = new_resolution; + global_state.settings.save_to_file_warn(); + }, + HudEvent::ChangeBitDepth(new_bit_depth) => { + global_state.window.fullscreen( + global_state.settings.graphics.fullscreen, + global_state.settings.graphics.resolution, + new_bit_depth, + global_state.settings.graphics.refresh_rate, + ); + global_state.settings.graphics.bit_depth = new_bit_depth; + global_state.settings.save_to_file_warn(); + }, + HudEvent::ChangeRefreshRate(new_refresh_rate) => { + global_state.window.fullscreen( + global_state.settings.graphics.fullscreen, + global_state.settings.graphics.resolution, + global_state.settings.graphics.bit_depth, + new_refresh_rate, + ); + global_state.settings.graphics.refresh_rate = new_refresh_rate; + global_state.settings.save_to_file_warn(); + }, HudEvent::ChangeLanguage(new_language) => { global_state.settings.language.selected_language = new_language.language_identifier; diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 43b0f2f19c..86b3c0c9e7 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -619,6 +619,9 @@ pub struct GraphicsSettings { pub aa_mode: AaMode, pub cloud_mode: CloudMode, pub fluid_mode: FluidMode, + pub resolution: [u16; 2], + pub bit_depth: Option, + pub refresh_rate: Option, pub window_size: [u16; 2], pub fullscreen: bool, } @@ -636,6 +639,9 @@ impl Default for GraphicsSettings { aa_mode: AaMode::Fxaa, cloud_mode: CloudMode::Regular, fluid_mode: FluidMode::Shiny, + resolution: [1920, 1080], + bit_depth: None, + refresh_rate: None, window_size: [1920, 1080], fullscreen: false, } diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index b8a6f913c4..8b43a2d0c1 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -7,11 +7,13 @@ use crate::{ use crossbeam::channel; use gilrs::{EventType, Gilrs}; use hashbrown::HashMap; +use itertools::Itertools; use old_school_gfx_glutin_ext::{ContextBuilderExt, WindowInitExt, WindowUpdateExt}; use serde_derive::{Deserialize, Serialize}; use std::fmt; use tracing::{error, info, warn}; use vek::*; +use winit::monitor::VideoMode; /// Represents a key that the game recognises after input mapping. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] @@ -610,7 +612,12 @@ impl Window { toggle_fullscreen: false, }; - this.fullscreen(settings.graphics.fullscreen); + this.fullscreen( + settings.graphics.fullscreen, + settings.graphics.resolution, + settings.graphics.bit_depth, + settings.graphics.refresh_rate, + ); Ok((this, event_loop)) } @@ -1056,34 +1063,206 @@ impl Window { } pub fn toggle_fullscreen(&mut self, settings: &mut Settings) { - self.fullscreen(!self.is_fullscreen()); + self.fullscreen( + !self.is_fullscreen(), + settings.graphics.resolution, + settings.graphics.bit_depth, + settings.graphics.refresh_rate, + ); settings.graphics.fullscreen = self.is_fullscreen(); settings.save_to_file_warn(); } pub fn is_fullscreen(&self) -> bool { self.fullscreen } - pub fn fullscreen(&mut self, fullscreen: bool) { + pub fn select_video_mode_rec( + &self, + resolution: [u16; 2], + bit_depth: Option, + refresh_rate: Option, + correct_res: Option>, + correct_depth: Option>, + correct_rate: Option>, + ) -> Option { + // if a previous iteration of this method filtered the available video modes for + // the correct resolution already, load that value, otherwise filter it + // in this iteration + let correct_res = correct_res.unwrap_or_else(|| { + let window = self.window.window(); + window + .current_monitor() + .video_modes() + .filter(|mode| mode.size().width == resolution[0] as u32) + .filter(|mode| mode.size().height == resolution[1] as u32) + .collect() + }); + + match bit_depth { + // A bit depth is given + Some(depth) => { + // analogous to correct_res + let correct_depth = correct_depth.unwrap_or_else(|| { + correct_res + .iter() + .find(|mode| mode.bit_depth() == depth) + .cloned() + }); + + match refresh_rate { + // A bit depth and a refresh rate is given + Some(rate) => { + // analogous to correct_res + let correct_rate = correct_rate.unwrap_or_else(|| { + correct_res + .iter() + .find(|mode| mode.refresh_rate() == rate) + .cloned() + }); + + // if no video mode with the given bit depth and refresh rate exists, fall + // back to a video mode that fits the resolution and either bit depth or + // refresh rate depending on which parameter was causing the correct video + // mode not to be found + correct_res + .iter() + .filter(|mode| mode.bit_depth() == depth) + .find(|mode| mode.refresh_rate() == rate) + .cloned() + .or_else(|| { + if correct_depth.is_none() && correct_rate.is_none() { + warn!( + "Bit depth and refresh rate specified in settings are \ + incompatible with the monitor. Choosing highest bit \ + depth and refresh rate possible instead." + ); + } + + self.select_video_mode_rec( + resolution, + correct_depth.is_some().then_some(depth), + correct_rate.is_some().then_some(rate), + Some(correct_res), + Some(correct_depth), + Some(correct_rate), + ) + }) + }, + // A bit depth and no refresh rate is given + // if no video mode with the given bit depth exists, fall + // back to a video mode that fits only the resolution + None => match correct_depth { + Some(mode) => Some(mode), + None => { + warn!( + "Bit depth specified in settings is incompatible with the \ + monitor. Choosing highest bit depth possible instead." + ); + + self.select_video_mode_rec( + resolution, + None, + None, + Some(correct_res), + Some(correct_depth), + None, + ) + }, + }, + } + }, + // No bit depth is given + None => match refresh_rate { + // No bit depth and a refresh rate is given + Some(rate) => { + // analogous to correct_res + let correct_rate = correct_rate.unwrap_or_else(|| { + correct_res + .iter() + .find(|mode| mode.refresh_rate() == rate) + .cloned() + }); + + // if no video mode with the given bit depth exists, fall + // back to a video mode that fits only the resolution + match correct_rate { + Some(mode) => Some(mode), + None => { + warn!( + "Refresh rate specified in settings is incompatible with the \ + monitor. Choosing highest refresh rate possible instead." + ); + + self.select_video_mode_rec( + resolution, + None, + None, + Some(correct_res), + None, + Some(correct_rate), + ) + }, + } + }, + // No bit depth and no refresh rate is given + // get the video mode with the specified resolution and the max bit depth and + // refresh rate + None => correct_res + .into_iter() + // Prefer bit depth over refresh rate + .sorted_by_key(|mode| mode.bit_depth()) + .max_by_key(|mode| mode.refresh_rate()), + }, + } + } + + pub fn select_video_mode( + &self, + resolution: [u16; 2], + bit_depth: Option, + refresh_rate: Option, + ) -> VideoMode { + // (resolution, bit depth, refresh rate) represents a video mode + // spec: as specified + // max: maximum value available + + // order of fallbacks as follows: + // (spec, spec, spec) + // (spec, spec, max), (spec, max, spec) + // (spec, max, max) + // (max, max, max) + self.select_video_mode_rec(resolution, bit_depth, refresh_rate, None, None, None) + // if there is no video mode with the specified resolution, fall back to the video mode with max resolution, bit depth and refresh rate + .unwrap_or_else(|| { + warn!( + "Resolution specified in settings is incompatible with the monitor. Choosing \ + highest resolution possible instead." + ); + + self + .window + .window() + .current_monitor() + .video_modes() + // Prefer bit depth over refresh rate + .sorted_by_key(|mode| mode.refresh_rate()) + .sorted_by_key(|mode| mode.bit_depth()) + .max_by_key(|mode| mode.size().width) + .expect("No video modes available!!") + }) + } + + pub fn fullscreen( + &mut self, + fullscreen: bool, + resolution: [u16; 2], + bit_depth: Option, + refresh_rate: Option, + ) { let window = self.window.window(); self.fullscreen = fullscreen; if fullscreen { window.set_fullscreen(Some(winit::window::Fullscreen::Exclusive( - window - .current_monitor() - .video_modes() - .filter(|mode| mode.bit_depth() >= 24 && mode.refresh_rate() >= 59) - .max_by_key(|mode| mode.size().width) - .unwrap_or_else(|| { - warn!( - "No video mode with a bit depth of at least 24 and a refresh rate of \ - at least 60Hz found" - ); - window - .current_monitor() - .video_modes() - .max_by_key(|mode| mode.size().width) - .expect("No video modes available!!") - }), + self.select_video_mode(resolution, bit_depth, refresh_rate), ))); } else { window.set_fullscreen(None); From d0937d337ede921f772cfc142755565954756c3a Mon Sep 17 00:00:00 2001 From: Songtronix Date: Mon, 10 Aug 2020 09:02:53 +0200 Subject: [PATCH 70/71] change: update readme --- README.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index 6ba3f97efa..0937deacf4 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,6 @@ Due to rapid developement stable versions become outdated fast and might be **in If you want to compile Veloren yourself, follow the instructions in our [Book](https://book.veloren.net/contributors/introduction.html). -### Packaging status - -#### Fedora - -[COPR repo](https://copr.fedorainfracloud.org/coprs/atim/veloren/): `sudo dnf copr enable atim/veloren -y && sudo dnf install veloren -y` - -#### Arch - -[AUR Airshipper](https://aur.archlinux.org/packages/airshipper-git): `yay -Syu airshipper-git` - -[AUR latest binary release](https://aur.archlinux.org/packages/veloren-bin/): `yay -Syu veloren-bin` - -[AUR latest release](https://aur.archlinux.org/packages/veloren/): `yay -Syu veloren` - -[AUR latest master](https://aur.archlinux.org/packages/veloren-git): `yay -Syu veloren-git` - ## F.A.Q. ### **Q:** How is this game licensed? @@ -68,7 +52,7 @@ If you want to compile Veloren yourself, follow the instructions in our [Book](h ### **Q:** Do you accept donations? -**A:** To keep Veloren a passion project free from financial incentives we will **only accept donations to cover server hosting expenses.** There is no way to donate yet. +**A:** You can support the project on our [OpenCollective Page](https://opencollective.com/veloren). ## Credit From d38aab800055df32a0bda56156a5f0a6310a5b9f Mon Sep 17 00:00:00 2001 From: scott-c Date: Mon, 10 Aug 2020 19:04:30 +0800 Subject: [PATCH 71/71] fix particle de-synchronisation with paused game --- voxygen/src/scene/particle.rs | 58 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 11e3b74a1d..f797ba9e89 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -16,7 +16,7 @@ use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::Rng; use specs::{Join, WorldExt}; -use std::time::{Duration, Instant}; +use std::time::Duration; use vek::*; pub struct ParticleMgr { @@ -72,17 +72,16 @@ impl ParticleMgr { pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { if scene_data.particles_enabled { - let now = Instant::now(); + // update timings + self.scheduler.maintain(scene_data.state.get_time()); // remove dead Particle - self.particles.retain(|p| p.alive_until > now); + self.particles + .retain(|p| p.alive_until > scene_data.state.get_time()); // add new Particle self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); - - // update timings - self.scheduler.maintain(); } else { // remove all particle lifespans self.particles.clear(); @@ -305,36 +304,49 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model, + timers: HashMap, + + last_known_time: f64, } impl HeartbeatScheduler { pub fn new() -> Self { HeartbeatScheduler { timers: HashMap::new(), + last_known_time: 0.0, } } /// updates the last elapsed times and elasped counts /// this should be called once, and only once per tick. - pub fn maintain(&mut self) { - for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() { - // the number of iterations since last update - *heartbeats = - // TODO: use nightly api once stable; https://github.com/rust-lang/rust/issues/63139 - (last_update.elapsed().as_secs_f32() / frequency.as_secs_f32()).floor() as u8; + pub fn maintain(&mut self, now: f64) { + self.last_known_time = now; - // Instant::now() minus the heart beat count precision, - // or alternatively as expressed below. - *last_update += frequency.mul_f32(*heartbeats as f32); - // Note: we want to preserve incomplete heartbeats, and include them - // in the next update. + for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() { + // the number of frequency cycles that have occurred. + let total_heartbeats = (now - *last_update) / frequency.as_secs_f64(); + + // exclude partial frequency cycles + let full_heartbeats = total_heartbeats.floor(); + + *heartbeats = full_heartbeats as u8; + + // the remaining partial freqency cycle, as a decimal. + let partial_heartbeat = total_heartbeats - full_heartbeats; + + // the remaining partial freqency cycle, as a unit of time(f64). + let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64(); + + // now minus the left over heart beat count precision as seconds, + // Note: we want to preserve incomplete heartbeats, and roll them + // over into the next update. + *last_update = now - partial_heartbeat_as_time; } } @@ -345,9 +357,11 @@ impl HeartbeatScheduler { /// - if it's equal to the tick rate, it could be between 2 and 0, due to /// delta time variance. pub fn heartbeats(&mut self, frequency: Duration) -> u8 { + let last_known_time = self.last_known_time; + self.timers .entry(frequency) - .or_insert_with(|| (Instant::now(), 0)) + .or_insert_with(|| (last_known_time, 0)) .1 } @@ -355,14 +369,14 @@ impl HeartbeatScheduler { } struct Particle { - alive_until: Instant, // created_at + lifespan + alive_until: f64, // created_at + lifespan instance: ParticleInstance, } impl Particle { fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3) -> Self { Particle { - alive_until: Instant::now() + lifespan, + alive_until: time + lifespan.as_secs_f64(), instance: ParticleInstance::new(time, mode, pos), } }