diff --git a/.cargo/config b/.cargo/config index 6a9b75f52a..77c060848a 100644 --- a/.cargo/config +++ b/.cargo/config @@ -4,4 +4,7 @@ rustflags = [ ] [alias] -generate = "run --package tools --" \ No newline at end of file +generate = "run --package tools --" +test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features" +server = "run --bin veloren-server-cli" + diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c3409292..06d7096ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ 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 +- Some Campfire, fireball & bomb; particle, light & sound effects. +- Added setting to change resolution ### Changed @@ -80,6 +84,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 c5b34a2dfa..a43ad66af9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4465,7 +4465,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 0.11.2", @@ -4703,6 +4704,7 @@ dependencies = [ "roots", "serde", "serde_json", + "slab", "specs", "specs-idvs", "sum_type", @@ -4783,6 +4785,7 @@ dependencies = [ "guillotiere", "hashbrown", "image", + "itertools", "msgbox", "num 0.2.1", "old_school_gfx_glutin_ext", 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 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 0b00677652..4d0a7e98bf 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"), @@ -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/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index c2725ba3cd..e81459a2df 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.explosion", + ], + threshold: 0.2, + ), + ProjectileShot: ( + files: [ + "voxygen.audio.sfx.glider_open", + ], + threshold: 0.5, + ), } ) 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/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/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/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/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/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/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/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/de_DE.ron b/assets/voxygen/i18n/de_DE.ron index e267bbc22c..38c5de8b8e 100644 --- a/assets/voxygen/i18n/de_DE.ron +++ b/assets/voxygen/i18n/de_DE.ron @@ -68,6 +68,9 @@ VoxygenLocalization( "common.none": "Kein", "common.error": "Fehler", "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 @@ -295,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.lighting_rendering_mode": "Beleuchtung", "hud.settings.lighting_rendering_mode.ashikhmin": "Typ A", @@ -315,7 +322,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", @@ -323,6 +330,23 @@ 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_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.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", @@ -385,6 +409,9 @@ magischen Gegenstände ergattern?"#, "gameinput.freelook": "Freie Sicht", "gameinput.autowalk": "Automatisch Laufen", "gameinput.dance": "Tanzen", + "gameinput.declinegroupinvite": "Ablehnen", + "gameinput.acceptgroupinvite": "Annehmen", + "gameinput.select": "Auswählen", /// End GameInput section diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index aba20a4d32..56676d3aff 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -65,11 +65,14 @@ VoxygenLocalization( "common.create": "Create", "common.okay": "Okay", "common.accept": "Accept", + "common.decline": "Decline", "common.disclaimer": "Disclaimer", "common.cancel": "Cancel", "common.none": "None", "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! @@ -295,6 +298,10 @@ 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.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", "hud.settings.lighting_rendering_mode": "Lighting Rendering Mode", @@ -318,12 +325,16 @@ magically infused items?"#, "hud.settings.unbound": "None", "hud.settings.reset_keybinds": "Reset to Defaults", - "hud.social": "Social", - "hud.social.online": "Online", + "hud.social": "Other Players", + "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", @@ -331,7 +342,20 @@ 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.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.group.members": "Group Members", + + "hud.spell": "Spells", "hud.free_look_indicator": "Free look active", "hud.auto_walk_indicator": "Auto walk active", @@ -343,7 +367,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", @@ -389,7 +413,10 @@ magically infused items?"#, "gameinput.freelook": "Free Look", "gameinput.autowalk": "Auto Walk", "gameinput.dance": "Dance", - + "gameinput.select": "Select Entity", + "gameinput.acceptgroupinvite": "Accept Group Invite", + "gameinput.declinegroupinvite": "Decline Group Invite", + /// End GameInput section @@ -448,7 +475,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!", @@ -459,7 +487,9 @@ 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.", + "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/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 +) 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/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/shaders/include/random.glsl b/assets/voxygen/shaders/include/random.glsl index 8298012000..7f297866f2 100644 --- a/assets/voxygen/shaders/include/random.glsl +++ b/assets/voxygen/shaders/include/random.glsl @@ -1,7 +1,7 @@ uniform sampler2D t_noise; 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 babb39ab66..5ae73158fb 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -343,10 +343,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*/hash(vec4(pos, 1.0)); + 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) { diff --git a/assets/voxygen/shaders/particle-frag.glsl b/assets/voxygen/shaders/particle-frag.glsl new file mode 100644 index 0000000000..d7757cc29d --- /dev/null +++ b/assets/voxygen/shaders/particle-frag.glsl @@ -0,0 +1,86 @@ +#version 330 core + +#include + +#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION + +#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY + +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE + +#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET + +#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN + +#define HAS_SHADOW_MAPS + +#include + +in vec3 f_pos; +flat in vec3 f_norm; +in vec3 f_col; + +out vec4 tgt_color; + +#include +#include +#include + +const float FADE_DIST = 32.0; + +void main() { + vec3 cam_to_frag = normalize(f_pos - cam_pos.xyz); + vec3 view_dir = -cam_to_frag; + +#if (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_MAP || FLUID_MODE == FLUID_MODE_SHINY) + float f_alt = alt_at(f_pos.xy); +#elif (SHADOW_MODE == SHADOW_MODE_NONE || FLUID_MODE == FLUID_MODE_CHEAP) + float f_alt = f_pos.z; +#endif + +#if (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_MAP) + vec4 f_shadow = textureBicubic(t_horizon, pos_to_tex(f_pos.xy)); + float sun_shade_frac = horizon_at2(f_shadow, f_alt, f_pos, sun_dir); +#elif (SHADOW_MODE == SHADOW_MODE_NONE) + float sun_shade_frac = 1.0; +#endif + float moon_shade_frac = 1.0; + + float point_shadow = shadow_at(f_pos, f_norm); + DirectionalLight sun_info = get_sun_info(sun_dir, point_shadow * sun_shade_frac, f_pos); + DirectionalLight moon_info = get_moon_info(moon_dir, point_shadow * moon_shade_frac); + + vec3 surf_color = f_col; + float alpha = 1.0; + const float n2 = 1.5; + const float R_s2s0 = pow((1.0 - n2) / (1.0 + n2), 2); + const float R_s1s0 = pow((1.3325 - n2) / (1.3325 + n2), 2); + const float R_s2s1 = pow((1.0 - 1.3325) / (1.0 + 1.3325), 2); + const float R_s1s2 = pow((1.3325 - 1.0) / (1.3325 + 1.0), 2); + float R_s = (f_pos.z < f_alt) ? mix(R_s2s1 * R_s1s0, R_s1s0, medium.x) : mix(R_s2s0, R_s1s2 * R_s2s0, medium.x); + + vec3 k_a = vec3(1.0); + vec3 k_d = vec3(1.0); + vec3 k_s = vec3(R_s); + + vec3 emitted_light, reflected_light; + + // To account for prior saturation. + float max_light = 0.0; + max_light += get_sun_diffuse2(sun_info, moon_info, f_norm, view_dir, k_a, k_d, k_s, alpha, emitted_light, reflected_light); + + max_light += lights_at(f_pos, f_norm, view_dir, k_a, k_d, k_s, alpha, emitted_light, reflected_light); + + surf_color = illuminate(max_light, view_dir, surf_color * emitted_light, surf_color * reflected_light); + +#if (CLOUD_MODE == CLOUD_MODE_REGULAR) + float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); + vec4 clouds; + vec3 fog_color = get_sky_color(cam_to_frag, time_of_day.x, cam_pos.xyz, f_pos, 0.5, false, clouds); + vec3 color = mix(mix(surf_color, fog_color, fog_level), clouds.rgb, clouds.a); +#elif (CLOUD_MODE == CLOUD_MODE_NONE) + vec3 color = surf_color; +#endif + + 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/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl new file mode 100644 index 0000000000..54b1cd7615 --- /dev/null +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -0,0 +1,140 @@ +#version 330 core + +#include + +#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION + +#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY + +#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE + +#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET + +#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN + +#include +#include +#include + +in vec3 v_pos; +// in uint v_col; +in uint v_norm_ao; +in vec3 inst_pos; +in float inst_time; +in float inst_entropy; +in int inst_mode; + +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; + +// Modes +const int SMOKE = 0; +const int FIRE = 1; +const int GUN_POWDER_SPARK = 2; +const int SHRAPNEL = 3; + +// meters per second squared (acceleration) +const float earth_gravity = 9.807; + +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() { + 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)); + + Attr attr; + + if (inst_mode == SMOKE) { + 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) { + 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) { + 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 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( + 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_pos - focus_off.xyz) + (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_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); + + gl_Position = + all_mat * + vec4(f_pos, 1); + gl_Position.z = -1000.0 / (gl_Position.z + 10000.0); +} 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/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/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/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/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/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/client/src/lib.rs b/client/src/lib.rs index 17ae9b0237..3b9b0fee31 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -17,14 +17,15 @@ use byteorder::{ByteOrder, LittleEndian}; use common::{ character::CharacterItem, comp::{ - self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip, - InventoryUpdateEvent, + self, group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, + InventoryManip, InventoryUpdateEvent, }, msg::{ - validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, - PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, - MAX_BYTES_CHAT_MSG, + validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, InviteAnswer, + Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, + ServerMsg, MAX_BYTES_CHAT_MSG, }, + outcome::Outcome, recipe::RecipeBook, state::State, sync::{Uid, UidAllocator, WorldSyncExt}, @@ -68,6 +69,7 @@ pub enum Event { InventoryUpdated(InventoryUpdateEvent), Notification(Notification), SetViewDistance(u32), + Outcome(Outcome), } pub struct Client { @@ -102,6 +104,15 @@ pub struct Client { recipe_book: RecipeBook, available_recipes: HashSet, + 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, singleton_stream: Stream, @@ -149,166 +160,170 @@ impl Client { let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?; // Wait for initial sync - let (state, entity, server_info, lod_base, lod_alt, lod_horizon, world_map, recipe_book) = - block_on(async { - loop { - match stream.recv().await? { - ServerMsg::InitialSync { - entity_package, - server_info, - time_of_day, - 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, + lod_base, + lod_alt, + lod_horizon, + 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, + 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; - let map_size_lg = common::terrain::MapSizeLg::new( - world_map.dimensions_lg, - ) + let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg) .map_err(|_| { Error::Other(format!( "Server sent bad world map dimensions: {:?}", world_map.dimensions_lg, )) })?; - let map_size = map_size_lg.chunks(); - let max_height = world_map.max_height; - let sea_level = world_map.sea_level; - let rgba = world_map.rgba; - let alt = world_map.alt; - let expected_size = - (u32::from(map_size.x) * u32::from(map_size.y)) as usize; - if rgba.len() != expected_size { - return Err(Error::Other( - "Server sent a bad world map image".into(), - )); - } - if alt.len() != expected_size { - return Err(Error::Other("Server sent a bad altitude map.".into())); - } - let [west, east] = world_map.horizons; - let scale_angle = - |a: u8| (a as f32 / 255.0 * ::FRAC_PI_2()).tan(); - let scale_height = |h: u8| h as f32 / 255.0 * max_height; - let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; + let map_size = map_size_lg.chunks(); + let max_height = world_map.max_height; + let sea_level = world_map.sea_level; + let rgba = world_map.rgba; + let alt = world_map.alt; + let expected_size = + (u32::from(map_size.x) * u32::from(map_size.y)) as usize; + if rgba.len() != expected_size { + return Err(Error::Other("Server sent a bad world map image".into())); + } + if alt.len() != expected_size { + return Err(Error::Other("Server sent a bad altitude map.".into())); + } + let [west, east] = world_map.horizons; + let scale_angle = + |a: u8| (a as f32 / 255.0 * ::FRAC_PI_2()).tan(); + let scale_height = |h: u8| h as f32 / 255.0 * max_height; + let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; - debug!("Preparing image..."); - let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| { - ( - angles.iter().copied().map(scale_angle).collect::>(), - heights - .iter() - .copied() - .map(scale_height) - .collect::>(), - ) - }; - let horizons = [unzip_horizons(&west), unzip_horizons(&east)]; + debug!("Preparing image..."); + let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| { + ( + angles.iter().copied().map(scale_angle).collect::>(), + heights + .iter() + .copied() + .map(scale_height) + .collect::>(), + ) + }; + let horizons = [unzip_horizons(&west), unzip_horizons(&east)]; - // Redraw map (with shadows this time). - let mut world_map = vec![0u32; rgba.len()]; - let mut map_config = common::terrain::map::MapConfig::orthographic( - map_size_lg, - core::ops::RangeInclusive::new(0.0, max_height), - ); - map_config.horizons = Some(&horizons); - let rescale_height = |h: f32| h / max_height; - let bounds_check = |pos: Vec2| { - pos.reduce_partial_min() >= 0 - && pos.x < map_size.x as i32 - && pos.y < map_size.y as i32 - }; - map_config.generate( - |pos| { - let (rgba, alt, downhill_wpos) = if bounds_check(pos) { - let posi = - pos.y as usize * map_size.x as usize + pos.x as usize; - let [r, g, b, a] = rgba[posi].to_le_bytes(); - let alti = alt[posi]; - // Compute downhill. - let downhill = { - let mut best = -1; - let mut besth = alti; - for nposi in neighbors(map_size_lg, posi) { - let nbh = alt[nposi]; - if nbh < besth { - besth = nbh; - best = nposi as isize; - } + // Redraw map (with shadows this time). + let mut world_map = vec![0u32; rgba.len()]; + let mut map_config = common::terrain::map::MapConfig::orthographic( + map_size_lg, + core::ops::RangeInclusive::new(0.0, max_height), + ); + map_config.horizons = Some(&horizons); + let rescale_height = |h: f32| h / max_height; + let bounds_check = |pos: Vec2| { + pos.reduce_partial_min() >= 0 + && pos.x < map_size.x as i32 + && pos.y < map_size.y as i32 + }; + map_config.generate( + |pos| { + let (rgba, alt, downhill_wpos) = if bounds_check(pos) { + let posi = + pos.y as usize * map_size.x as usize + pos.x as usize; + let [r, g, b, a] = rgba[posi].to_le_bytes(); + let alti = alt[posi]; + // Compute downhill. + let downhill = { + let mut best = -1; + let mut besth = alti; + for nposi in neighbors(map_size_lg, posi) { + let nbh = alt[nposi]; + if nbh < besth { + besth = nbh; + best = nposi as isize; } - best - }; - let downhill_wpos = if downhill < 0 { - None - } else { - Some( - Vec2::new( - (downhill as usize % map_size.x as usize) - as i32, - (downhill as usize / map_size.x as usize) - as i32, - ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), - ) - }; - (Rgba::new(r, g, b, a), alti, downhill_wpos) - } else { - (Rgba::zero(), 0, None) + } + best }; - let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let downhill_wpos = downhill_wpos.unwrap_or( - wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32), - ); - let alt = rescale_height(scale_height_big(alt)); - common::terrain::map::MapSample { - rgb: Rgb::from(rgba), - alt: f64::from(alt), - downhill_wpos, - connections: None, - } - }, - |wpos| { - let pos = - wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); - rescale_height(if bounds_check(pos) { - let posi = - pos.y as usize * map_size.x as usize + pos.x as usize; - scale_height_big(alt[posi]) + let downhill_wpos = if downhill < 0 { + None } else { - 0.0 - }) - }, - |pos, (r, g, b, a)| { - world_map[pos.y * map_size.x as usize + pos.x] = - u32::from_le_bytes([r, g, b, a]); - }, - ); - let make_raw = |rgba| -> Result<_, Error> { - let mut raw = vec![0u8; 4 * world_map.len()]; - LittleEndian::write_u32_into(rgba, &mut raw); - Ok(Arc::new( - image::DynamicImage::ImageRgba8({ + Some( + Vec2::new( + (downhill as usize % map_size.x as usize) as i32, + (downhill as usize / map_size.x as usize) as i32, + ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ) + }; + (Rgba::new(r, g, b, a), alti, downhill_wpos) + } else { + (Rgba::zero(), 0, None) + }; + let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let downhill_wpos = downhill_wpos.unwrap_or( + wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ); + let alt = rescale_height(scale_height_big(alt)); + common::terrain::map::MapSample { + rgb: Rgb::from(rgba), + alt: f64::from(alt), + downhill_wpos, + connections: None, + } + }, + |wpos| { + let pos = + wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + rescale_height(if bounds_check(pos) { + let posi = + pos.y as usize * map_size.x as usize + pos.x as usize; + scale_height_big(alt[posi]) + } else { + 0.0 + }) + }, + |pos, (r, g, b, a)| { + world_map[pos.y * map_size.x as usize + pos.x] = + u32::from_le_bytes([r, g, b, a]); + }, + ); + let make_raw = |rgba| -> Result<_, Error> { + let mut raw = vec![0u8; 4 * world_map.len()]; + LittleEndian::write_u32_into(rgba, &mut raw); + Ok(Arc::new( + image::DynamicImage::ImageRgba8({ // Should not fail if the dimensions are correct. let map = image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw); @@ -317,37 +332,38 @@ 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(), - )) - }; - let lod_base = rgba; - let lod_alt = alt; - let world_map = make_raw(&world_map)?; - let horizons = (west.0, west.1, east.0, east.1) - .into_par_iter() - .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh])) - .collect::>(); - let lod_horizon = horizons; - let map_bounds = Vec2::new(sea_level, max_height); - debug!("Done preparing image..."); + )) + }; + let lod_base = rgba; + let lod_alt = alt; + let world_map = make_raw(&world_map)?; + let horizons = (west.0, west.1, east.0, east.1) + .into_par_iter() + .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh])) + .collect::>(); + let lod_horizon = horizons; + let map_bounds = Vec2::new(sea_level, max_height); + debug!("Done preparing image..."); - break Ok(( - state, - entity, - server_info, - lod_base, - lod_alt, - lod_horizon, - (world_map, map_size, map_bounds), - recipe_book, - )); - }, - ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), - err => { - warn!("whoops, server mad {:?}, ignoring", err); - }, - } + break Ok(( + state, + entity, + server_info, + lod_base, + lod_alt, + lod_horizon, + (world_map, map_size, map_bounds), + recipe_book, + max_group_size, + )); + }, + ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), + err => { + warn!("whoops, server mad {:?}, ignoring", err); + }, } - })?; + } + })?; stream.send(ClientMsg::Ping)?; @@ -371,6 +387,12 @@ impl Client { 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), singleton_stream: stream, @@ -533,7 +555,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), @@ -582,6 +604,72 @@ impl Client { .unwrap(); } + 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 + } + + 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( + GroupManip::Invite(invitee), + ))) + .unwrap() + } + + 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 decline_group_invite(&mut self) { + // Clear invite + self.group_invite.take(); + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Decline, + ))) + .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, uid: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Kick(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 { self.state .ecs() @@ -591,7 +679,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(); @@ -667,6 +755,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 @@ -848,6 +951,13 @@ 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); @@ -1093,7 +1203,102 @@ 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, 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 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 \ + were already there", + uid + ); + } + }, + 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 \ + 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 includes 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 { 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)?; }, @@ -1134,7 +1339,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); @@ -1200,6 +1405,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)) + }, } } } @@ -1244,6 +1452,9 @@ 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 } @@ -1295,7 +1506,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 @@ -1306,8 +1517,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!!!"); @@ -1378,7 +1588,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/Cargo.toml b/common/Cargo.toml index 8485a57467..d08669a0de 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -31,6 +31,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" } +slab = "0.4.2" [dev-dependencies] criterion = "0.3" diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 812af83795..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, @@ -50,7 +51,6 @@ pub enum ChatCommand { Health, Help, JoinFaction, - JoinGroup, Jump, Kill, KillNpcs, @@ -80,6 +80,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, ChatCommand::Alias, ChatCommand::Build, + ChatCommand::Campfire, ChatCommand::Debug, ChatCommand::DebugColumn, ChatCommand::Dummy, @@ -92,7 +93,6 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Health, ChatCommand::Help, ChatCommand::JoinFaction, - ChatCommand::JoinGroup, ChatCommand::Jump, ChatCommand::Kill, ChatCommand::KillNpcs, @@ -187,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)], @@ -246,11 +247,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), @@ -372,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", @@ -383,7 +380,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/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/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/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/controller.rs b/common/src/comp/controller.rs index 01942d68f1..ff00ac4b49 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, + Decline, + Leave, + Kick(Uid), + AssignLeader(Uid), +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum ControlEvent { ToggleLantern, Mount(Uid), Unmount, InventoryManip(InventoryManip), + GroupManip(GroupManip), Respawn, } @@ -35,6 +46,7 @@ pub enum ControlAction { Unwield, Sit, Dance, + Sneak, Stand, } @@ -159,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, } @@ -183,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) { @@ -195,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 @@ -209,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/comp/group.rs b/common/src/comp/group.rs new file mode 100644 index 0000000000..bf11ad7f58 --- /dev/null +++ b/common/src/comp/group.rs @@ -0,0 +1,528 @@ +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 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); + +// 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>; +} + +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, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum Role { + Member, + Pet, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ChangeNotification { + // :D + Added(E, Role), + // :( + Removed(E), + NewLeader(E), + // Use to put in a group overwriting existing group + NewGroup { leader: E, members: Vec<(E, Role)> }, + // 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, 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(|(e, r)| f(e).map(|t| (t, r))) + .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 +// Note: iterating through all entities here could become slow at higher entity +// counts +fn pets( + entity: specs::Entity, + uid: Uid, + alignments: &Alignments, + entities: &specs::Entities, +) -> Vec { + (entities, alignments) + .join() + .filter_map(|(e, a)| { + matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e) + }) + .collect::>() +} + +/// Returns list of current members of a group +pub fn members<'a>( + group: Group, + groups: impl Join + 'a, + entities: &'a specs::Entities, + alignments: &'a Alignments, + uids: &'a Uids, +) -> impl Iterator + 'a { + (entities, groups, alignments, uids) + .join() + .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 +impl GroupManager { + pub fn group_info(&self, group: Group) -> Option<&GroupInfo> { + self.groups.get(group.0 as usize) + } + + 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) + } + + 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 + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + 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), + ) { + // 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 + } 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) + .and_then(|g| self.group_info(*g)) + .is_some() + { + self.leave_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.leave_group(leader, groups, alignments, uids, entities, &mut notifier); + None + }, + None => None, + }; + + 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); + + // Inform + 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)); + }, + }); + 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 + let _ = groups.insert(new_member, group).unwrap(); + new_pets.iter().for_each(|e| { + let _ = groups.insert(*e, group).unwrap(); + }); + } + + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + pub fn new_pet( + &mut self, + pet: specs::Entity, + 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() { + Some(group) => group, + None => { + let new_group = self.create_group(owner, 1); + groups.insert(owner, new_group).unwrap(); + // Inform + notifier(owner, ChangeNotification::NewLeader(owner)); + new_group + }, + }; + + // Inform + 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(); + } + + pub fn leave_group( + &mut self, + member: specs::Entity, + groups: &mut GroupsMut, + alignments: &Alignments, + 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) + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + 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, + 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(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member)) + .fold( + HashMap::, Vec)>::new(), + |mut acc, (e, uid, _, alignment)| { + if let Some(owner) = alignment.and_then(|a| match a { + 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); + } + + acc + }, + ) + .into_iter() + .map(|(_, v)| v) + .for_each(|(owner, pets)| { + if let Some(owner) = owner { + if !pets.is_empty() { + 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, 1); + for (member, _) in &members { + groups.insert(*member, new_group).unwrap(); + } + + notifier(owner, ChangeNotification::NewGroup { + leader: owner, + members, + }); + } else { + // If no pets just remove group + groups.remove(owner); + notifier(owner, ChangeNotification::NoGroup) + } + } else { + // Owner not found, potentially the were removed from the world + pets.into_iter().for_each(|pet| { + groups.remove(pet); + }); + } + }); + } else { + // Not leader + let leaving_member_uid = if let Some(uid) = uids.get(member) { + *uid + } else { + error!("Failed to retrieve uid for the leaving member"); + return; + }; + + let leaving_pets = pets(member, leaving_member_uid, alignments, entities); + + // 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, 1); + + notifier(member, ChangeNotification::NewGroup { + leader: member, + members: leaving_pets + .iter() + .map(|p| (*p, Role::Pet)) + .chain(std::iter::once((member, Role::Member))) + .collect(), + }); + + let _ = groups.insert(member, new_group).unwrap(); + leaving_pets.iter().for_each(|&e| { + let _ = groups.insert(e, new_group).unwrap(); + }); + } else { + let _ = groups.remove(member); + notifier(member, ChangeNotification::NoGroup); + leaving_pets.iter().for_each(|&e| { + let _ = groups.remove(e); + }); + } + + if let Some(info) = self.group_info_mut(group) { + // 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)| { + remaining_count += 1; + 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 + if remaining_count == 1 { + let leader = info.leader; + self.remove_group(group); + groups.remove(leader); + notifier(leader, ChangeNotification::NoGroup); + } else if remaining_count == 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, + alignments: &Alignments, + uids: &Uids, + 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, alignments, uids).for_each(|(e, role)| match role { + Role::Member => notifier(e, ChangeNotification::NewLeader(new_leader)), + Role::Pet => {}, + }); + } +} diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 401f9925c7..b040f0517f 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -261,6 +261,7 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), + projectile_gravity: None, }, BasicRanged { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index b99b575450..963f3c95a6 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; @@ -17,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}; @@ -28,13 +29,16 @@ 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}; +pub use chat::{ + ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, +}; 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/comp/phys.rs b/common/src/comp/phys.rs index e9496f047c..466734cb4c 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 } +} + impl Component for Ori { type Storage = IdvStorage; } diff --git a/common/src/event.rs b/common/src/event.rs index 7bcb6683db..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, @@ -35,6 +36,7 @@ pub enum ServerEvent { cause: comp::HealthSource, }, InventoryManip(EcsEntity, comp::InventoryManip), + GroupManip(EcsEntity, comp::GroupManip), Respawn(EcsEntity), Shoot { entity: EcsEntity, @@ -80,7 +82,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/lib.rs b/common/src/lib.rs index 628e562400..8945831e3b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -26,6 +26,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/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/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 aaf0bd3477..1d8f0660ad 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, @@ -168,6 +169,13 @@ pub struct WorldMapMsg { pub horizons: [(Vec, Vec); 2], } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum InviteAnswer { + Accepted, + Declined, + TimedOut, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Notification { WaypointSaved, @@ -180,6 +188,7 @@ pub enum ServerMsg { entity_package: sync::EntityPackage, server_info: ServerInfo, time_of_day: state::TimeOfDay, + max_group_size: u32, world_map: WorldMapMsg, recipe_book: RecipeBook, }, @@ -190,6 +199,22 @@ pub enum ServerMsg { /// An error occured while creating or deleting a character CharacterActionError(String), PlayerListUpdate(PlayerListUpdate), + GroupUpdate(comp::group::ChangeNotification), + // 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 @@ -217,6 +242,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..4f89f418b0 --- /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/common/src/state.rs b/common/src/state.rs index f232d07163..3d003b7a4f 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,8 +157,9 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); ecs.register::(); + ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); @@ -168,9 +170,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 @@ -196,8 +199,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/common/src/states/climb.rs b/common/src/states/climb.rs index 3a5ca7093e..4bf3cf71b2 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,15 @@ 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.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); 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/agent.rs b/common/src/sys/agent.rs index 36b8afd4e9..e2cb88b750 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -2,9 +2,12 @@ use crate::{ comp::{ self, agent::Activity, + group, + group::Invite, item::{tool::ToolKind, ItemKind}, - Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout, - MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel, + Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, + GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, + Vel, }, event::{EventBus, ServerEvent}, path::{Chaser, TraversalConfig}, @@ -29,6 +32,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,12 +44,14 @@ 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>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, ReadStorage<'a, MountState>, + ReadStorage<'a, Invite>, ); #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -55,6 +61,7 @@ impl<'a> System<'a> for Sys { uid_allocator, time, dt, + group_manager, event_bus, entities, positions, @@ -66,12 +73,14 @@ impl<'a> System<'a> for Sys { character_states, physics_states, uids, + groups, terrain, alignments, bodies, mut agents, mut controllers, mount_states, + invites, ): Self::SystemData, ) { for ( @@ -88,6 +97,7 @@ impl<'a> System<'a> for Sys { agent, controller, mount_state, + group, ) in ( &entities, &positions, @@ -102,9 +112,23 @@ 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 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 .map(|ms| *ms != MountState::Unmounted) @@ -117,7 +141,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 +172,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 +191,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 +290,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; } @@ -418,8 +438,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 { @@ -437,7 +458,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())?; @@ -477,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/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/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/common/src/sys/phys.rs b/common/src/sys/phys.rs index c065a9621f..0df848919c 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -1,16 +1,21 @@ 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; -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 @@ -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..20f96fc311 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, ) { @@ -72,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 { @@ -92,23 +91,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 { @@ -143,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/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/server/src/cmd.rs b/server/src/cmd.rs index d65c62ab29..5a282766fd 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -2,12 +2,12 @@ //! 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, 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}, @@ -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, @@ -77,7 +78,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, @@ -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 { @@ -557,6 +557,43 @@ 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(), + &state.ecs().read_storage(), + &uids, + &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) + }, + 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, @@ -594,7 +631,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), @@ -628,6 +665,39 @@ 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_copied::(target) { + Some(pos) => { + server + .state + .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( + 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, @@ -961,13 +1031,14 @@ 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 { pos: pos.0, power, owner: ecs.read_storage::().get(target).copied(), + friendly_damage: true, }) }, None => server.notify_client( @@ -984,7 +1055,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 @@ -1020,7 +1091,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); @@ -1161,8 +1232,8 @@ fn handle_group( return; } let ecs = server.state.ecs(); - if let Some(comp::Group(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) { @@ -1172,7 +1243,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"), ); } } @@ -1323,68 +1394,6 @@ fn handle_join_faction( } } -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::Group(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::Group(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, @@ -1626,7 +1635,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_creation.rs b/server/src/events/entity_creation.rs index f2ea1ddc63..206136aadc 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -4,8 +4,10 @@ use common::{ self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, + outcome::Outcome, util::Dir, }; +use comp::group; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -36,12 +38,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 { @@ -75,10 +91,18 @@ 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 259cd87859..ff6f4ae2b6 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2,17 +2,18 @@ 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, Alignment, Body, Damage, DamageSource, Group, + HealthChange, HealthSource, Player, Pos, Stats, }, msg::{PlayerListUpdate, ServerMsg}, + outcome::Outcome, 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; @@ -55,28 +56,88 @@ 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 - { - state.ecs().entity_from_uid(by.into()).map(|attacker| { - 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, - ); - } - }); - } + 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; + }; + + 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::(); + 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 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, _, _)| { + // 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, _, _, alignment, uid)| { + if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) { + num_not_pets_in_range += 1; + } + + entity + }) + .collect::>(); + 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) { + 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() @@ -189,7 +250,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); @@ -217,11 +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(); - for (pos_b, ori_b, character_b, stats_b, loadout_b) in ( + + // Add an outcome + ecs.write_resource::>() + .push(Outcome::Explosion { pos, power }); + + 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(), @@ -233,9 +312,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/group_manip.rs b/server/src/events/group_manip.rs new file mode 100644 index 0000000000..fb028729de --- /dev/null +++ b/server/src/events/group_manip.rs @@ -0,0 +1,452 @@ +use crate::{client::Client, Server}; +use common::{ + comp::{ + self, + group::{self, Group, GroupManager, Invite, PendingInvites}, + ChatType, GroupManip, + }, + msg::{InviteAnswer, ServerMsg}, + sync, + sync::WorldSyncExt, +}; +use specs::world::WorldExt; +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 { + 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("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; + } + + // Disallow inviting entity that is already in your group + let groups = state.ecs().read_storage::(); + let group_manager = state.ecs().read_resource::(); + 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( + "Invite failed, can't invite someone already in your group".to_owned(), + )); + } + return; + } + + let mut pending_invites = state.ecs().write_storage::(); + + // Check if group max size is already reached + // Adding the current number of pending invites + let group_size_limit_reached = 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; + 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( + 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( + ChatType::Meta + .server_msg("This player already has a pending invite.".to_owned()), + ); + } + return; + } + + 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::(); + 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, + 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::Decline => { + let mut clients = state.ecs().write_storage::(); + 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), Some(target)) = + (clients.get_mut(inviter), uids.get(entity).copied()) + { + client.notify(ServerMsg::InviteComplete { + target, + answer: InviteAnswer::Declined, + }) + } + } + }, + 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.leave_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 alignments = 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("Kick failed, target does not exist.".to_owned()), + ); + } + 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, you can't kick pets.".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, you 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 + match groups + .get(target) + .and_then(|group| group_manager.group_info(*group)) + { + Some(info) if info.leader == entity => { + // Remove target from group + group_manager.leave_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("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("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(), + )); + } + }, + 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(), + &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))); + }, + ); + // Tell them they are the leader + if let Some(client) = clients.get_mut(target) { + 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("You are no longer the group leader.".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..549804d6f8 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, @@ -166,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, )); @@ -184,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::(), @@ -222,6 +223,35 @@ 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(), + &state.ecs().read_storage(), + &uids, + &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() @@ -311,7 +341,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, )); @@ -343,10 +373,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(), )); @@ -377,7 +407,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/mod.rs b/server/src/events/mod.rs index fb26dad411..412d9a536a 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; @@ -48,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, @@ -62,6 +67,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..2c9a06e54a 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}, }; @@ -20,8 +20,13 @@ 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() + .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,39 @@ 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(), + &state.ecs().read_storage(), + &state.ecs().read_storage(), + // Nothing actually changing since Uid is transferred + |_, _| {}, + ); + } + } } + // 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 de17f2663f..25ee1a90e7 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; @@ -34,6 +34,7 @@ use common::{ comp::{self, ChatType}, event::{EventBus, ServerEvent}, msg::{server::WorldMapMsg, 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()); @@ -127,6 +129,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 @@ -513,12 +516,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 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 @@ -580,6 +589,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"]) @@ -689,6 +702,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: 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/state_ext.rs b/server/src/state_ext.rs index 645e7843d4..77b42d53a0 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( @@ -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!!!"); @@ -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)) }, 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::(), ) .join() { - if s == &group.0 { - client.notify(ServerMsg::ChatMsg(msg.clone())); + if g == group { + client.notify(ServerMsg::ChatMsg(resolved_msg.clone())); } } }, @@ -346,6 +354,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.entity_deleted( + 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/entity_sync.rs b/server/src/sys/entity_sync.rs index d4bbf62d20..2dc20648b4 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, Player, Pos, Vel}, msg::ServerMsg, + outcome::Outcome, region::{Event as RegionEvent, RegionMap}, state::TimeOfDay, sync::{CompSyncPackage, Uid}, + terrain::TerrainChunkSize, + vol::RectVolSize, }; 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,26 @@ 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/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/message.rs b/server/src/sys/message.rs index d2d7c895d6..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, ChatMsg, 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::{ @@ -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/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/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, }); } }, 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::(); diff --git a/server/src/test_world.rs b/server/src/test_world.rs index 77f01d91c1..e286e46bb4 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, MapSizeLg, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, vol::{ReadVol, RectVolSize, Vox, WriteVol}, }; @@ -38,17 +38,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(), ), diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index f20228c1ce..e5bbae44eb 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -60,7 +60,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" @@ -71,6 +71,7 @@ deunicode = "1.0" uvth = "3.1.1" # vec_map = { version = "0.8.2" } const-tweaker = { version = "0.3.1", optional = true } +itertools = "0.9.0" # Logging tracing = "0.1" diff --git a/voxygen/src/anim/Cargo.toml b/voxygen/src/anim/Cargo.toml index d643592c9c..221cb791ec 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 d8cf301761..dffe3d56ae 100644 --- a/voxygen/src/anim/src/biped_large/idle.rs +++ b/voxygen/src/anim/src/biped_large/idle.rs @@ -39,6 +39,8 @@ impl Animation for IdleAnimation { * 0.25, ); + let wave_slow = (anim_time as f32 * 0.8).sin(); + next.head.position = Vec3::new( 0.0, skeleton_attr.head.0, @@ -64,10 +66,27 @@ impl Animation for IdleAnimation { next.lower_torso.orientation = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.position = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.orientation = Quaternion::rotation_x(wave_slow * 0.09); + next.jaw.scale = Vec3::one(); + + next.tail.position = Vec3::new( + 0.0, + skeleton_attr.tail.0, + skeleton_attr.tail.1 + torso * 0.0, + ); + next.tail.orientation = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.control.position = Vec3::new(0.0, 0.0, 0.0); next.control.orientation = Quaternion::rotation_z(0.0); next.control.scale = Vec3::one(); + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.main.position = Vec3::new(-5.0, -7.0, 7.0); next.main.orientation = 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 d0a5b1f080..6c29bc4094 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.orientation = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.position = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.orientation = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.position = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1 * 0.0); + next.tail.orientation = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.shoulder_l.position = 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 1743ca3329..cb5b346550 100644 --- a/voxygen/src/anim/src/biped_large/mod.rs +++ b/voxygen/src/anim/src/biped_large/mod.rs @@ -14,9 +14,12 @@ use core::convert::TryFrom; skeleton_impls!(struct BipedLargeSkeleton { + head, + + jaw, + upper_torso, + lower_torso, + + tail, + main, + + second, + shoulder_l, + shoulder_r, + hand_l, @@ -32,7 +35,7 @@ skeleton_impls!(struct BipedLargeSkeleton { impl Skeleton for BipedLargeSkeleton { type Attr = SkeletonAttr; - const BONE_COUNT: usize = 12; + const BONE_COUNT: usize = 15; #[cfg(feature = "use-dyn-lib")] const COMPUTE_FN: &'static [u8] = b"biped_large_compute_mats\0"; @@ -48,12 +51,16 @@ impl Skeleton for BipedLargeSkeleton { let control_mat = torso_mat * Mat4::::from(self.control) * upper_torso; let upper_torso_mat = torso_mat * upper_torso; let lower_torso_mat = upper_torso_mat * Mat4::::from(self.lower_torso); + let head_mat = upper_torso_mat * Mat4::::from(self.head); *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ - make_bone(upper_torso_mat * Mat4::::from(self.head)), + make_bone(head_mat), + make_bone(head_mat * Mat4::::from(self.jaw)), make_bone(upper_torso_mat), make_bone(lower_torso_mat), + make_bone(lower_torso_mat * Mat4::::from(self.tail)), make_bone(control_mat * Mat4::::from(self.main)), + make_bone(control_mat * Mat4::::from(self.second)), make_bone(upper_torso_mat * Mat4::::from(self.shoulder_l)), make_bone(upper_torso_mat * Mat4::::from(self.shoulder_r)), make_bone(control_mat * Mat4::::from(self.hand_l)), @@ -69,8 +76,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), @@ -92,8 +101,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), @@ -113,6 +124,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), @@ -127,11 +145,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 d3c71f907b..ede6b30ebb 100644 --- a/voxygen/src/anim/src/biped_large/run.rs +++ b/voxygen/src/anim/src/biped_large/run.rs @@ -81,6 +81,19 @@ impl Animation for RunAnimation { Quaternion::rotation_z(short * 0.15) * Quaternion::rotation_x(0.14); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.position = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1); + next.jaw.orientation = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.position = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1 * 0.0); + next.tail.orientation = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.control.position = Vec3::new(0.0, 0.0, 0.0); next.control.orientation = 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 7dc23a4dc0..0b9813aade 100644 --- a/voxygen/src/anim/src/biped_large/wield.rs +++ b/voxygen/src/anim/src/biped_large/wield.rs @@ -79,6 +79,11 @@ impl Animation for WieldAnimation { * Quaternion::rotation_z(1.0); next.main.scale = Vec3::one() * 1.02; + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = + Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0); + next.second.scale = Vec3::one() * 0.0; + next.hand_l.position = Vec3::new( -skeleton_attr.hand.0 - 7.0, skeleton_attr.hand.1 - 7.0, @@ -123,6 +128,14 @@ impl Animation for WieldAnimation { Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; + next.jaw.position = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1 * 0.0); + next.jaw.orientation = Quaternion::rotation_z(0.0); + next.jaw.scale = Vec3::one(); + + next.tail.position = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1); + next.tail.orientation = Quaternion::rotation_z(0.0); + next.tail.scale = Vec3::one(); + next.shoulder_l.position = Vec3::new( -skeleton_attr.shoulder.0, skeleton_attr.shoulder.1, diff --git a/voxygen/src/anim/src/character/climb.rs b/voxygen/src/anim/src/character/climb.rs index c7a510c9ae..ec4c311202 100644 --- a/voxygen/src/anim/src/character/climb.rs +++ b/voxygen/src/anim/src/character/climb.rs @@ -3,7 +3,7 @@ use super::{ CharacterSkeleton, SkeletonAttr, }; use common::comp::item::{Hands, ToolKind}; -use std::f32::consts::PI; +use std::{f32::consts::PI, ops::Mul}; pub struct ClimbAnimation; @@ -23,19 +23,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 lateral = Vec2::::from(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))) @@ -48,148 +49,277 @@ 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.position = 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.orientation = 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; + let stagnant = if speed > -0.7 { 1.0 } else { 0.0 }; //sets static position when there is no movement - next.chest.position = Vec3::new( - 0.0, - skeleton_attr.chest.0 + 1.0, - skeleton_attr.chest.1 + smootha * 1.1, - ); - next.chest.orientation = Quaternion::rotation_z(quick * 0.25) - * Quaternion::rotation_x(-0.15) - * Quaternion::rotation_y(quick * -0.12); - next.chest.scale = Vec3::one(); - - next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0 + 1.0, skeleton_attr.belt.1); - next.belt.orientation = Quaternion::rotation_z(quick * 0.0) * Quaternion::rotation_x(0.0); - next.belt.scale = Vec3::one(); - - next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.orientation = Quaternion::rotation_x(-0.2); - next.back.scale = Vec3::one() * 1.02; - - next.shorts.position = Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); - next.shorts.orientation = 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.position = 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.orientation = Quaternion::rotation_x(2.2 + quicka * 0.5); - next.l_hand.scale = Vec3::one(); - - next.r_hand.position = 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.orientation = Quaternion::rotation_x(2.2 - quicka * 0.5); - next.r_hand.scale = Vec3::one(); - - next.l_foot.position = Vec3::new( - -skeleton_attr.foot.0, - 1.0 + skeleton_attr.foot.1, - skeleton_attr.foot.2 + quick * 2.5, - ); - next.l_foot.orientation = Quaternion::rotation_x(0.2 - quicka * 0.5); - next.l_foot.scale = Vec3::one(); - - next.r_foot.position = Vec3::new( - skeleton_attr.foot.0, - 1.0 + skeleton_attr.foot.1, - skeleton_attr.foot.2 - quick * 2.5, - ); - next.r_foot.orientation = Quaternion::rotation_x(0.2 + quicka * 0.5); - next.r_foot.scale = Vec3::one(); - - next.l_shoulder.position = Vec3::new( - -skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.l_shoulder.orientation = Quaternion::rotation_x(smootha * 0.15); - next.l_shoulder.scale = Vec3::one() * 1.1; - - next.r_shoulder.position = Vec3::new( - skeleton_attr.shoulder.0, - skeleton_attr.shoulder.1, - skeleton_attr.shoulder.2, - ); - next.r_shoulder.orientation = Quaternion::rotation_x(smooth * 0.15); - next.r_shoulder.scale = Vec3::one() * 1.1; - - next.glider.position = Vec3::new(0.0, 0.0, 10.0); - next.glider.scale = Vec3::one() * 0.0; - - match active_tool_kind { - Some(ToolKind::Dagger(_)) => { - next.main.position = Vec3::new(-4.0, -5.0, 7.0); - next.main.orientation = - Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(1.5 * PI); - }, - Some(ToolKind::Shield(_)) => { - next.main.position = Vec3::new(-0.0, -5.0, 3.0); - next.main.orientation = - Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); - }, - _ => { - next.main.position = Vec3::new(-7.0, -5.0, 15.0); - next.main.orientation = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - }, - } - next.main.scale = Vec3::one(); - - match second_tool_kind { - Some(ToolKind::Dagger(_)) => { - next.second.position = Vec3::new(4.0, -6.0, 7.0); - next.second.orientation = - Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); - }, - Some(ToolKind::Shield(_)) => { - next.second.position = Vec3::new(0.0, -4.0, 3.0); - next.second.orientation = - Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(1.5 * PI); - }, - _ => { - next.second.position = Vec3::new(-7.0, -5.0, 15.0); - next.second.orientation = - Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); - }, - } - next.second.scale = Vec3::one(); - - next.lantern.position = Vec3::new( - skeleton_attr.lantern.0, - skeleton_attr.lantern.1, - skeleton_attr.lantern.2, - ); - next.lantern.orientation = - Quaternion::rotation_x(smooth * -0.3) * Quaternion::rotation_y(smooth * -0.3); - next.lantern.scale = Vec3::one() * 0.65; next.hold.scale = Vec3::one() * 0.0; - next.torso.position = Vec3::new(0.0, -0.2 + smooth * -0.08, 0.4) * skeleton_attr.scaler; - next.torso.orientation = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); - next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + if speed > 0.7 || lateral > 0.1 { + next.head.position = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + skeleton_attr.head.1 + smootha * 0.2, + ); + next.head.orientation = 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.position = Vec3::new( + 0.0, + skeleton_attr.chest.0, + skeleton_attr.chest.1 + smootha * 1.1, + ); + next.chest.orientation = Quaternion::rotation_z(quick * 0.25) + * Quaternion::rotation_x(-0.15) + * Quaternion::rotation_y(quick * -0.12); + next.chest.scale = Vec3::one(); + + next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0 + 1.0, skeleton_attr.belt.1); + next.belt.orientation = + Quaternion::rotation_z(quick * 0.0) * Quaternion::rotation_x(0.0); + next.belt.scale = Vec3::one(); + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.orientation = Quaternion::rotation_x(-0.2); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.position = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.orientation = 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.position = 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.orientation = Quaternion::rotation_x(2.2 + quicka * 0.5); + next.l_hand.scale = Vec3::one(); + + next.r_hand.position = 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.orientation = Quaternion::rotation_x(2.2 - quicka * 0.5); + next.r_hand.scale = Vec3::one(); + + match active_tool_kind { + Some(ToolKind::Dagger(_)) => { + next.main.position = Vec3::new(-4.0, -5.0, 7.0); + next.main.orientation = + Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(1.5 * PI); + }, + Some(ToolKind::Shield(_)) => { + next.main.position = Vec3::new(-0.0, -5.0, 3.0); + next.main.orientation = + Quaternion::rotation_y(0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); + }, + _ => { + next.main.position = Vec3::new(-7.0, -5.0, 15.0); + next.main.orientation = + Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + }, + } + next.main.scale = Vec3::one(); + + match second_tool_kind { + Some(ToolKind::Dagger(_)) => { + next.second.position = Vec3::new(4.0, -6.0, 7.0); + next.second.orientation = + Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(-1.5 * PI); + }, + Some(ToolKind::Shield(_)) => { + next.second.position = Vec3::new(0.0, -4.0, 3.0); + next.second.orientation = + Quaternion::rotation_y(-0.25 * PI) * Quaternion::rotation_z(1.5 * PI); + }, + _ => { + next.second.position = Vec3::new(-7.0, -5.0, 15.0); + next.second.orientation = + Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + }, + } + next.second.scale = Vec3::one(); + next.l_foot.position = Vec3::new( + -skeleton_attr.foot.0, + 5.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2 + quick * 2.5, + ); + next.l_foot.orientation = Quaternion::rotation_x(0.2 - quicka * 0.5); + next.l_foot.scale = Vec3::one(); + + next.r_foot.position = Vec3::new( + skeleton_attr.foot.0, + 4.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2 - quick * 2.5, + ); + next.r_foot.orientation = Quaternion::rotation_x(0.2 + quicka * 0.5); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.position = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.orientation = Quaternion::rotation_x(smootha * 0.15); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.position = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.orientation = Quaternion::rotation_x(smooth * 0.15); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.glider.position = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.main.position = Vec3::new(-7.0, -5.0, 18.0); + next.main.orientation = + Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57 + smootha * 0.25); + next.main.scale = Vec3::one(); + + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.position = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.orientation = + Quaternion::rotation_x(smooth * -0.3) * Quaternion::rotation_y(smooth * -0.3); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.position = Vec3::new(0.0, -0.2 + smooth * -0.08, 0.4) * skeleton_attr.scaler; + next.torso.orientation = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + } else { + next.head.position = Vec3::new( + 0.0, + -1.0 - stagnant + skeleton_attr.head.0, + skeleton_attr.head.1, + ); + next.head.orientation = 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.position = + Vec3::new(0.0, 1.0 + skeleton_attr.chest.0, skeleton_attr.chest.1); + next.chest.orientation = Quaternion::rotation_z(0.6 * stagnant) + * Quaternion::rotation_x((0.2 + drop * 0.05) * (1.0 - stagnant)); + next.chest.scale = Vec3::one(); + + next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0 + 0.5, skeleton_attr.belt.1); + next.belt.orientation = Quaternion::rotation_x(0.1 + dropa * 0.1); + next.belt.scale = Vec3::one(); + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.orientation = 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.position = + Vec3::new(0.0, skeleton_attr.shorts.0 + 1.0, skeleton_attr.shorts.1); + next.shorts.orientation = Quaternion::rotation_x(0.1 + dropa * 0.12 * (1.0 - stagnant)); + next.shorts.scale = Vec3::one(); + + next.l_hand.position = 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.orientation = 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.position = 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.orientation = 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.position = 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.orientation = Quaternion::rotation_x(0.55 + drop * 0.1 * (1.0 - stagnant)); + next.l_foot.scale = Vec3::one(); + + next.r_foot.position = 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.orientation = + Quaternion::rotation_x(0.2 + smooth * 0.15 * (1.0 - stagnant)); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.position = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.orientation = Quaternion::rotation_x(0.0); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.position = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.orientation = Quaternion::rotation_x(0.0); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.glider.position = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.main.position = Vec3::new(-7.0, -5.0, 18.0); + next.main.orientation = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = Quaternion::rotation_y(0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.position = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.orientation = Quaternion::rotation_x(0.0); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.position = Vec3::new(0.0, -0.2, 0.4) * skeleton_attr.scaler; + next.torso.orientation = 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.l_control.scale = Vec3::one(); next.second.scale = match ( active_tool_kind.map(|tk| tk.hands()), diff --git a/voxygen/src/anim/src/character/mod.rs b/voxygen/src/anim/src/character/mod.rs index 5bc1ac616b..4709eac25b 100644 --- a/voxygen/src/anim/src/character/mod.rs +++ b/voxygen/src/anim/src/character/mod.rs @@ -16,10 +16,12 @@ pub mod roll; pub mod run; pub mod shoot; pub mod sit; +pub mod sneak; pub mod spin; pub mod spinmelee; pub mod stand; pub mod swim; +pub mod swimwield; pub mod wield; // Reexports @@ -29,8 +31,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, + swimwield::SwimWieldAnimation, wield::WieldAnimation, }; use super::{make_bone, vek::*, Bone, FigureBoneData, Skeleton}; diff --git a/voxygen/src/anim/src/character/run.rs b/voxygen/src/anim/src/character/run.rs index 7bbedaefe9..10ad471fd4 100644 --- a/voxygen/src/anim/src/character/run.rs +++ b/voxygen/src/anim/src/character/run.rs @@ -50,17 +50,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/sneak.rs b/voxygen/src/anim/src/character/sneak.rs new file mode 100644 index 0000000000..83b44d82f3 --- /dev/null +++ b/voxygen/src/anim/src/character/sneak.rs @@ -0,0 +1,325 @@ +use super::{ + super::{vek::*, Animation}, + CharacterSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; +use std::{f32::consts::PI, ops::Mul}; + +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")] + + 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 orientation: Vec2 = Vec2::from(orientation); + let last_ori = Vec2::from(last_ori); + let tilt = if ::vek::Vec2::new(orientation, last_ori) + .map(|o| o.magnitude_squared()) + .map(|m| m > 0.001 && m.is_finite()) + .reduce_and() + && orientation.angle_between(last_ori).is_finite() + { + orientation.angle_between(last_ori).min(0.2) + * last_ori.determine_side(Vec2::zero(), orientation).signum() + } else { + 0.0 + } * 1.3; + + next.hold.scale = Vec3::one() * 0.0; + + if speed > 0.5 { + next.l_hand.position = Vec3::new( + 1.0 - skeleton_attr.hand.0, + 4.0 + skeleton_attr.hand.1, + 1.0 + skeleton_attr.hand.2, + ); + next.l_hand.orientation = Quaternion::rotation_x(1.0); + next.l_hand.scale = Vec3::one(); + + next.r_hand.position = Vec3::new( + -1.0 + skeleton_attr.hand.0, + -1.0 + skeleton_attr.hand.1, + skeleton_attr.hand.2, + ); + next.r_hand.orientation = Quaternion::rotation_x(0.4); + next.r_hand.scale = Vec3::one(); + next.head.position = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + -1.0 + skeleton_attr.head.1 + short * 0.06, + ); + next.head.orientation = + 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.position = Vec3::new( + 0.0, + skeleton_attr.chest.0, + -1.0 + skeleton_attr.chest.1 + shortalt * -0.5, + ); + next.chest.orientation = 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.position = + Vec3::new(0.0, 0.5 + skeleton_attr.belt.0, 0.7 + skeleton_attr.belt.1); + next.belt.orientation = 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.orientation = Quaternion::rotation_x(0.0); + next.glider.position = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.orientation = + Quaternion::rotation_x(-0.25 + short * 0.1 + noisea * 0.1 + noiseb * 0.1); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.position = Vec3::new( + 0.0, + 1.0 + skeleton_attr.shorts.0, + 1.0 + skeleton_attr.shorts.1, + ); + next.shorts.orientation = 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.position = 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.orientation = + Quaternion::rotation_x(-0.2 + footrotl * -0.8 * walkintensity) + * Quaternion::rotation_y(tilt * 1.8); + next.l_foot.scale = Vec3::one(); + + next.r_foot.position = 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.orientation = + Quaternion::rotation_x(-0.2 + footrotr * -0.8 * walkintensity) + * Quaternion::rotation_y(tilt * 1.8); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.position = Vec3::new( + -skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.l_shoulder.orientation = Quaternion::rotation_x(short * 0.15 * walkintensity); + next.l_shoulder.scale = Vec3::one() * 1.1; + + next.r_shoulder.position = Vec3::new( + skeleton_attr.shoulder.0, + skeleton_attr.shoulder.1, + skeleton_attr.shoulder.2, + ); + next.r_shoulder.orientation = Quaternion::rotation_x(short * -0.15 * walkintensity); + next.r_shoulder.scale = Vec3::one() * 1.1; + + next.main.position = Vec3::new(-7.0, -6.5, 15.0); + next.main.orientation = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.scale = Vec3::one() * 0.0; + + next.lantern.position = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.orientation = + Quaternion::rotation_x(shorte * 0.2 + 0.4) * Quaternion::rotation_y(shorte * 0.1); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.position = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.orientation = Quaternion::rotation_y(0.0); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.control.position = Vec3::new(0.0, 0.0, 0.0); + next.control.orientation = 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.position = Vec3::new( + 0.0, + -4.0 + skeleton_attr.head.0, + -2.0 + skeleton_attr.head.1 + slow * 0.1 + breathe * -0.05, + ); + next.head.orientation = 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.position = Vec3::new( + 0.0, + skeleton_attr.chest.0, + -3.0 + skeleton_attr.chest.1 + slow * 0.1, + ); + next.chest.orientation = Quaternion::rotation_x(-0.7); + next.chest.scale = Vec3::one() * 1.01 + breathe * 0.03; + + next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.orientation = Quaternion::rotation_z(0.3 + head_look.x * -0.1); + next.belt.scale = Vec3::one() + breathe * -0.03; + + next.l_hand.position = Vec3::new( + 1.0 - skeleton_attr.hand.0, + 5.0 + skeleton_attr.hand.1, + 0.0 + skeleton_attr.hand.2, + ); + next.l_hand.orientation = Quaternion::rotation_x(1.35); + next.l_hand.scale = Vec3::one(); + + next.r_hand.position = Vec3::new( + -1.0 + skeleton_attr.hand.0, + skeleton_attr.hand.1, + skeleton_attr.hand.2, + ); + next.r_hand.orientation = Quaternion::rotation_x(0.4); + next.r_hand.scale = Vec3::one(); + + next.glider.orientation = Quaternion::rotation_x(0.35); + next.glider.position = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.scale = Vec3::one() * 1.02; + + next.shorts.position = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.orientation = Quaternion::rotation_z(0.6 + head_look.x * -0.2); + next.shorts.scale = Vec3::one() + breathe * -0.03; + + next.l_foot.position = Vec3::new( + -skeleton_attr.foot.0, + -6.0 + skeleton_attr.foot.1, + 1.0 + skeleton_attr.foot.2, + ); + next.l_foot.orientation = Quaternion::rotation_x(-0.5); + next.l_foot.scale = Vec3::one(); + + next.r_foot.position = Vec3::new( + skeleton_attr.foot.0, + 4.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + next.r_foot.orientation = Quaternion::rotation_x(0.0); + next.r_foot.scale = Vec3::one(); + + next.l_shoulder.position = 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.position = 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.position = Vec3::new(-7.0, -5.0, 15.0); + next.main.orientation = Quaternion::rotation_y(2.5) * Quaternion::rotation_z(1.57); + next.main.scale = Vec3::one(); + + next.second.position = Vec3::new(0.0, 0.0, 0.0); + next.second.scale = Vec3::one() * 0.0; + + next.lantern.position = Vec3::new( + skeleton_attr.lantern.0, + skeleton_attr.lantern.1, + skeleton_attr.lantern.2, + ); + next.lantern.orientation = Quaternion::rotation_x(0.1) * Quaternion::rotation_y(0.1); + next.lantern.scale = Vec3::one() * 0.65; + + next.torso.position = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.orientation = 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 5e087bf6de..dfaacc2501 100644 --- a/voxygen/src/anim/src/character/swim.rs +++ b/voxygen/src/anim/src/character/swim.rs @@ -14,6 +14,7 @@ type SwimAnimationDependency = ( Vec3, Vec3, f64, + Vec3, ); impl Animation for SwimAnimation { @@ -27,33 +28,50 @@ 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 avgspeed = Vec2::::from(avg_vel).magnitude(); - let speed = Vec2::::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 }; - let lab = 1.0; + 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 * 0.9).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 + PI * -0.1).sin(); - let foot = (anim_time as f32 * lab as f32 * 6.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 wave_stop = (anim_time as f32 * 9.0).min(PI / 2.0 / 2.0).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.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 / 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() @@ -67,72 +85,84 @@ 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 } * 1.3; + let abstilt = tilt.abs(); + + let squash = if abstilt > 0.2 { 0.35 } else { 1.0 }; //condenses the body at strong turns next.head.position = Vec3::new( 0.0, -3.0 + skeleton_attr.head.0, skeleton_attr.head.1 - 1.0 + short * 0.3, ); - next.head.orientation = Quaternion::rotation_z(head_look.x - short * 0.4) - * Quaternion::rotation_x(head_look.y + 0.35 + speed * 0.045); + next.head.orientation = + 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).min(0.0), + ); next.head.scale = Vec3::one() * skeleton_attr.head_scale; next.chest.position = Vec3::new( 0.0, skeleton_attr.chest.0, - skeleton_attr.chest.1 + short * 1.3, + -10.0 + skeleton_attr.chest.1 + short * 0.3 * intensity, ); - next.chest.orientation = Quaternion::rotation_z(short * 0.4); + next.chest.orientation = Quaternion::rotation_z(short * 0.1 * intensity); next.chest.scale = Vec3::one(); next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); - next.belt.orientation = Quaternion::rotation_z(short * 0.30); + next.belt.orientation = 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.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); - next.back.orientation = Quaternion::rotation_z(0.0); next.back.scale = Vec3::one() * 1.02; next.shorts.position = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); - next.shorts.orientation = Quaternion::rotation_z(short * 0.5); + next.shorts.orientation = 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.position = 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.0 - skeleton_attr.hand.0, + 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.orientation = - Quaternion::rotation_x(0.8 + foot * -0.6) * Quaternion::rotation_y(0.2); + next.l_hand.orientation = 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.position = 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.0 + skeleton_attr.hand.0, + 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.orientation = - Quaternion::rotation_x(0.8 + foot * 0.6) * Quaternion::rotation_y(-0.2); + next.r_hand.orientation = 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.position = 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 + foothoril * 1.5 * intensity * squash, + -10.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity * squash, ); - next.l_foot.orientation = Quaternion::rotation_x(-1.1 + foot * 0.6); + next.l_foot.orientation = + Quaternion::rotation_x(-0.8 * squash + footrotl * 0.4 * intensity * squash); next.l_foot.scale = Vec3::one(); next.r_foot.position = 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 * squash, + -10.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity * squash, ); - next.r_foot.orientation = Quaternion::rotation_x(-1.1 + foot * -0.6); + next.r_foot.orientation = + Quaternion::rotation_x(-0.8 * squash + footrotr * 0.4 * intensity * squash); next.r_foot.scale = Vec3::one(); next.l_shoulder.position = Vec3::new( @@ -140,7 +170,7 @@ impl Animation for SwimAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.l_shoulder.orientation = Quaternion::rotation_x(short * 0.15); + next.l_shoulder.orientation = Quaternion::rotation_x(short * 0.15 * intensity); next.l_shoulder.scale = Vec3::one() * 1.1; next.r_shoulder.position = Vec3::new( @@ -148,7 +178,7 @@ impl Animation for SwimAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.r_shoulder.orientation = Quaternion::rotation_x(short * -0.15); + next.r_shoulder.orientation = Quaternion::rotation_x(short * -0.15 * intensity); next.r_shoulder.scale = Vec3::one() * 1.1; next.glider.position = Vec3::new(0.0, 0.0, 10.0); @@ -200,12 +230,20 @@ impl Animation for SwimAnimation { next.lantern.scale = Vec3::one() * 0.65; next.hold.scale = Vec3::one() * 0.0; - next.torso.position = Vec3::new(0.0, -1.2 + shortalt * -0.065, 0.4) * skeleton_attr.scaler; - next.torso.orientation = Quaternion::rotation_x(speed * -0.190 * wave_stop * 1.05) - * Quaternion::rotation_z(tilt * 12.0); + let switch = if avg_vel.z > 0.0 && avgspeed < 0.5 { + avgtotal.min(0.5) + } else { + avgtotal + }; + next.torso.position = Vec3::new(0.0, 0.0, 1.0 - avgspeed * 0.05) * skeleton_attr.scaler; + next.torso.orientation = 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_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(); + 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 new file mode 100644 index 0000000000..4e0f2f4b72 --- /dev/null +++ b/voxygen/src/anim/src/character/swimwield.rs @@ -0,0 +1,438 @@ +use super::{ + super::{vek::*, Animation}, + CharacterSkeleton, SkeletonAttr, +}; +use common::comp::item::{Hands, ToolKind}; +use std::{f32::consts::PI, ops::Mul}; + +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.position = Vec3::new( + -skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothoril * 1.5 * intensity, + -10.0 + skeleton_attr.foot.2 + footrotl * 3.0 * intensity, + ); + next.l_foot.orientation = Quaternion::rotation_x(-0.8 + footrotl * 0.4 * intensity); + next.l_foot.scale = Vec3::one(); + + next.r_foot.position = Vec3::new( + skeleton_attr.foot.0, + skeleton_attr.foot.1 + foothorir * 1.5 * intensity, + -10.0 + skeleton_attr.foot.2 + footrotr * 3.0 * intensity, + ); + next.r_foot.orientation = Quaternion::rotation_x(-0.8 + footrotr * 0.4 * intensity); + next.r_foot.scale = Vec3::one(); + + next.hold.scale = Vec3::one() * 0.0; + + if velocity > 0.01 { + next.torso.position = Vec3::new(0.0, 0.0, 1.0) * skeleton_attr.scaler; + next.torso.orientation = Quaternion::rotation_x(velocity * -0.05); + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.orientation = 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.position = Vec3::new( + 0.0, + -2.0 + skeleton_attr.head.0, + skeleton_attr.head.1 + u_slow * 0.1, + ); + next.head.orientation = + Quaternion::rotation_z(head_look.x) * Quaternion::rotation_x(head_look.y.abs()); + next.head.scale = Vec3::one() * skeleton_attr.head_scale; + + next.chest.position = Vec3::new( + 0.0 + slowalt * 0.5, + skeleton_attr.chest.0, + skeleton_attr.chest.1 + u_slow * 0.5, + ); + next.torso.position = Vec3::new(0.0, 0.0, 0.0) * skeleton_attr.scaler; + next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler; + + next.l_foot.position = Vec3::new( + -skeleton_attr.foot.0, + -2.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + + next.r_foot.position = Vec3::new( + skeleton_attr.foot.0, + 2.0 + skeleton_attr.foot.1, + skeleton_attr.foot.2, + ); + + next.chest.orientation = + Quaternion::rotation_y(u_slowalt * 0.04) * Quaternion::rotation_z(0.25); + + next.belt.position = Vec3::new(0.0, skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.orientation = + Quaternion::rotation_y(u_slowalt * 0.03) * Quaternion::rotation_z(0.22); + next.belt.scale = Vec3::one() * 1.02; + + next.back.position = Vec3::new(0.0, skeleton_attr.back.0, skeleton_attr.back.1); + next.back.orientation = Quaternion::rotation_x(-0.2); + next.back.scale = Vec3::one() * 1.02; + next.shorts.position = Vec3::new(0.0, skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.orientation = Quaternion::rotation_z(0.3); + } + match active_tool_kind { + //TODO: Inventory + Some(ToolKind::Sword(_)) => { + next.l_hand.position = Vec3::new(-0.75, -1.0, -2.5); + next.l_hand.orientation = + Quaternion::rotation_x(1.47) * Quaternion::rotation_y(-0.2); + next.l_hand.scale = Vec3::one() * 1.04; + next.r_hand.position = Vec3::new(0.75, -1.5, -5.5); + next.r_hand.orientation = + Quaternion::rotation_x(1.47) * Quaternion::rotation_y(0.3); + next.r_hand.scale = Vec3::one() * 1.05; + next.main.position = Vec3::new(0.0, 0.0, -3.0); + next.main.orientation = Quaternion::rotation_x(-0.1) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + + next.control.position = Vec3::new(-7.0, 6.0, 6.0); + next.control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + //next.control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.l_hand.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.main.orientation = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + + next.l_control.position = Vec3::new(-7.0, 0.0, 0.0); + // next.l_control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.r_hand.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = 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.position = Vec3::new(7.0, 0.0, 0.0); + // next.r_control.orientation = 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.position = Vec3::new( + 0.0, + -3.5 + skeleton_attr.head.0, + skeleton_attr.head.1 + u_slow * 0.1, + ); + next.head.orientation = 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.orientation = Quaternion::rotation_x(-0.35) + * Quaternion::rotation_y(u_slowalt * 0.04) + * Quaternion::rotation_z(0.15); + next.belt.position = + Vec3::new(0.0, 1.0 + skeleton_attr.belt.0, skeleton_attr.belt.1); + next.belt.orientation = Quaternion::rotation_x(0.15) + * Quaternion::rotation_y(u_slowalt * 0.03) + * Quaternion::rotation_z(0.15); + next.shorts.position = + Vec3::new(0.0, 1.0 + skeleton_attr.shorts.0, skeleton_attr.shorts.1); + next.shorts.orientation = + Quaternion::rotation_x(0.15) * Quaternion::rotation_z(0.25); + next.control.orientation = Quaternion::rotation_x(1.8) + * Quaternion::rotation_y(-0.5) + * Quaternion::rotation_z(PI - 0.2); + next.control.scale = Vec3::one(); + } else { + next.control.orientation = Quaternion::rotation_x(2.1) + * Quaternion::rotation_y(-0.4) + * Quaternion::rotation_z(PI - 0.2); + next.control.scale = Vec3::one(); + } + next.l_hand.position = Vec3::new(-0.5, 0.0, 4.0); + next.l_hand.orientation = 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.position = Vec3::new(0.5, 0.0, -2.5); + next.r_hand.orientation = 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.position = Vec3::new(-0.0, -2.0, -1.0); + next.main.orientation = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(0.0); + + next.control.position = Vec3::new(-3.0, 11.0, 3.0); + }, + Some(ToolKind::Hammer(_)) => { + next.l_hand.position = Vec3::new(-12.0, 0.0, 0.0); + next.l_hand.orientation = + Quaternion::rotation_x(-0.0) * Quaternion::rotation_y(0.0); + next.l_hand.scale = Vec3::one() * 1.08; + next.r_hand.position = Vec3::new(2.0, 0.0, 0.0); + next.r_hand.orientation = Quaternion::rotation_x(0.0) * Quaternion::rotation_y(0.0); + next.r_hand.scale = Vec3::one() * 1.06; + next.main.position = Vec3::new(0.0, 0.0, 0.0); + next.main.orientation = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(-1.57) + * Quaternion::rotation_z(1.57); + + next.control.position = Vec3::new(6.0, 7.0, 1.0); + next.control.orientation = 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.position = Vec3::new(1.5, 0.5, -4.0); + next.l_hand.orientation = + Quaternion::rotation_x(1.47) * Quaternion::rotation_y(-0.3); + next.l_hand.scale = Vec3::one() * 1.05; + next.r_hand.position = Vec3::new(8.0, 4.0, 2.0); + next.r_hand.orientation = 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.position = Vec3::new(12.0, 8.4, 13.2); + next.main.orientation = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(3.14 + 0.3) + * Quaternion::rotation_z(0.9); + + next.control.position = Vec3::new(-14.0, 1.8, 3.0); + next.control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + // next.control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.l_hand.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.main.orientation = Quaternion::rotation_x(0.0 * PI) + * Quaternion::rotation_y(0.0 * PI) + * Quaternion::rotation_z(0.0 * PI); + + next.l_control.position = Vec3::new(-7.0, 0.0, 0.0); + // next.l_control.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.r_hand.orientation = 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.position = Vec3::new(0.0, 0.0, 0.0); + next.second.orientation = 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.position = Vec3::new(7.0, 0.0, 0.0); + // next.r_control.orientation = 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.position = Vec3::new(2.0, 1.5, 0.0); + next.l_hand.orientation = 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.position = Vec3::new(5.9, 4.5, -5.0); + next.r_hand.orientation = 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.position = Vec3::new(3.0, 2.0, -13.0); + next.main.orientation = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(0.3) + * Quaternion::rotation_z(-0.6); + + next.hold.position = Vec3::new(1.2, -1.0, -5.2); + next.hold.orientation = Quaternion::rotation_x(-1.7) + * Quaternion::rotation_y(0.0) + * Quaternion::rotation_z(-0.1); + next.hold.scale = Vec3::one() * 1.0; + + next.control.position = Vec3::new(-7.0, 6.0, 6.0); + next.control.orientation = + 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.position = Vec3::new(-7.0, 4.0, 3.0); + next.l_hand.orientation = 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.position = Vec3::new(7.0, 2.5, -1.25); + next.r_hand.orientation = 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.position = Vec3::new(5.0, 8.75, -2.0); + next.main.orientation = Quaternion::rotation_x(-0.3) + * Quaternion::rotation_y(-1.27) + * Quaternion::rotation_z(0.0); + next.main.scale = Vec3::one(); + next.control.position = Vec3::new(0.0, 6.0, 6.0); + next.control.orientation = + 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.orientation = 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.position = Vec3::new(9.0, 1.0, 1.0); + next.l_hand.orientation = + Quaternion::rotation_x(1.57) * Quaternion::rotation_y(0.0); + next.l_hand.scale = Vec3::one() * 1.05; + next.r_hand.position = Vec3::new(9.0, 1.0, 11.0); + next.r_hand.orientation = 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.position = Vec3::new(7.5, 7.5, 13.2); + next.main.orientation = Quaternion::rotation_x(0.0) + * Quaternion::rotation_y(3.14) + * Quaternion::rotation_z(0.0); + + next.control.position = Vec3::new(-11.0 + slow * 2.0, 1.8, 4.0); + next.control.orientation = 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/anim/src/quadruped_low/mod.rs b/voxygen/src/anim/src/quadruped_low/mod.rs index a6463fc007..d36eba621b 100644 --- a/voxygen/src/anim/src/quadruped_low/mod.rs +++ b/voxygen/src/anim/src/quadruped_low/mod.rs @@ -115,7 +115,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 8a0c8c48c0..08e5db484b 100644 --- a/voxygen/src/anim/src/quadruped_medium/mod.rs +++ b/voxygen/src/anim/src/quadruped_medium/mod.rs @@ -164,7 +164,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/anim/src/quadruped_small/run.rs b/voxygen/src/anim/src/quadruped_small/run.rs index 2cf079875e..cbf1092460 100644 --- a/voxygen/src/anim/src/quadruped_small/run.rs +++ b/voxygen/src/anim/src/quadruped_small/run.rs @@ -37,7 +37,7 @@ impl Animation for RunAnimation { let ori: Vec2 = Vec2::from(orientation); let last_ori = Vec2::from(last_ori); - let tilt = if vek::Vec2::new(ori, last_ori) + let tilt = if ::vek::Vec2::new(ori, last_ori) .map(|o| o.magnitude_squared()) .map(|m| m > 0.001 && m.is_finite()) .reduce_and() diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 8a88923f95..5f19e42532 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,16 @@ 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..56aca955b0 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,19 @@ 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(), @@ -57,9 +62,7 @@ impl EventMapper for CombatEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < 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 1628185479..d5a9d6f8cb 100644 --- a/voxygen/src/audio/sfx/event_mapper/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/mod.rs @@ -9,9 +9,16 @@ use movement::MovementEventMapper; use progression::ProgressionEventMapper; use super::SfxTriggers; +use crate::scene::Camera; 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 +40,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..55bff58c96 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,19 @@ 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(), @@ -55,9 +61,7 @@ impl EventMapper for MovementEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < 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 0b845e03a9..499e39e559 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,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, 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 b76c287cdb..783056271b 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -83,15 +83,16 @@ mod event_mapper; -use crate::audio::AudioFrontend; +use crate::{audio::AudioFrontend, scene::Camera}; use common::{ assets, comp::{ item::{ItemKind, ToolCategory}, - CharacterAbilityType, InventoryUpdateEvent, Ori, Pos, + CharacterAbilityType, InventoryUpdateEvent, }, event::EventBus, + outcome::Outcome, state::State, }; use event_mapper::SfxEventMapper; @@ -146,6 +147,8 @@ pub enum SfxEvent { Wield(ToolCategory), Unwield(ToolCategory), Inventory(SfxInventoryEvent), + Explosion, + ProjectileShot, } #[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)] @@ -224,36 +227,27 @@ 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); - let ecs = state.ecs(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir); - let player_ori = *ecs - .read_storage::() - .get(player_entity) - .copied() - .unwrap_or_default() - .0; - - audio.set_listener_pos(&player_position, &player_ori); + // TODO: replace; deprecated in favor of outcomes + self.event_mapper + .maintain(state, player_entity, camera, &self.triggers); + // TODO: replace; deprecated in favor of outcomes 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) { @@ -273,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, .. } => { + 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/hud/bag.rs b/voxygen/src/hud/bag.rs index 0af0b8b244..e3ab174156 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 @@ -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/buttons.rs b/voxygen/src/hud/buttons.rs index 8ae1f6b7c4..2c57697873 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)] @@ -360,6 +360,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 +397,7 @@ impl<'a> Widget for Buttons<'a> { .color(TEXT_COLOR) .set(state.ids.crafting_text, ui); } + None } } 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), 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 new file mode 100644 index 0000000000..26060b3084 --- /dev/null +++ b/voxygen/src/hud/group.rs @@ -0,0 +1,685 @@ +use super::{ + img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR, + MANA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN, +}; + +use crate::{ + i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts, + window::GameInput, GlobalState, +}; +use client::{self, Client}; +use common::{ + comp::{group::Role, Stats}, + sync::{Uid, WorldSyncExt}, +}; +use conrod_core::{ + color, + position::{Place, Relative}, + widget::{self, Button, Image, Rectangle, Scrollbar, Text}, + widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, +}; +use specs::{saveload::MarkerAllocator, WorldExt}; + +widget_ids! { + pub struct Ids { + group_button, + bg, + title, + title_bg, + btn_bg, + btn_friend, + btn_leader, + btn_link, + btn_kick, + btn_leave, + scroll_area, + scrollbar, + members[], + 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[], + timeout_bg, + timeout, + } +} + +pub struct State { + ids: Ids, + // Selected group member + selected_member: Option, +} + +#[derive(WidgetCommon)] +pub struct Group<'a> { + show: &'a mut Show, + client: &'a Client, + settings: &'a Settings, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + pulse: f32, + global_state: &'a GlobalState, + + #[conrod(common_builder)] + common: widget::CommonBuilder, +} + +impl<'a> Group<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + pub fn new( + show: &'a mut Show, + client: &'a Client, + settings: &'a Settings, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + pulse: f32, + global_state: &'a GlobalState, + ) -> Self { + Self { + show, + client, + settings, + imgs, + fonts, + localized_strings, + pulse, + global_state, + common: widget::CommonBuilder::default(), + } + } +} + +pub enum Event { + Accept, + Decline, + 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_member: None, + } + } + + 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; + + let mut events = Vec::new(); + + // 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::>(); + // 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; + self.show.group = false; + } + + // 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 self.show.group_menu || open_invite.is_some() { + // Frame + 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); + } + 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 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 * 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); + } + // Buttons + if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) { + // 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, 10.0, 490.0) + .hover_image(self.imgs.group_icon_hover) + .press_image(self.imgs.group_icon_press) + .set(state.ids.group_button, ui) + .was_clicked() + { + 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 { + 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 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 { + 290.0 + } else { + 110.0 + }; + let back = if i == 0 { + Image::new(self.imgs.member_bg) + .top_left_with_margins_on(ui.window, offset, 20.0) + } else { + Image::new(self.imgs.member_bg) + .down_from(state.ids.member_panels_bg[i - 1], 40.0) + }; + 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, + }; + let is_leader = uid == leader; + // Don't show panel for the player! + // Panel BG + back.w_h(152.0, 36.0) + .color(if is_leader { + Some(ERROR_COLOR) + } 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(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(KILL_COLOR) + .set(state.ids.dead_txt[i], ui); + } else { + // Health Text + let txt = format!( + "{}/{}", + 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, + 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(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(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(BLACK) + .w(300.0) // limit name length display + .set(state.ids.member_panels_txt_bg[i], ui); + Text::new(&char_name) + .bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(if is_leader { ERROR_COLOR } else { GROUP_COLOR }) + .w(300.0) // limit name length display + .set(state.ids.member_panels_txt[i], ui); + if let Some(energy) = energy { + let stam_perc = energy.current() as f64 / energy.maximum() as f64; + // Stamina + Image::new(self.imgs.bar_content) + .w_h(100.0 * stam_perc, 8.0) + .color(Some(MANA_COLOR)) + .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 back = if i == 0 { + Image::new(self.imgs.member_bg) + .top_left_with_margins_on(ui.window, offset, 20.0) + } else { + Image::new(self.imgs.member_bg) + .down_from(state.ids.member_panels_bg[i - 1], 40.0) + }; + back.w_h(152.0, 36.0) + .color(Some(TEXT_COLOR)) + .set(state.ids.member_panels_bg[i], ui); + // Panel Frame + Image::new(self.imgs.member_frame) + .w_h(152.0, 36.0) + .middle_of(state.ids.member_panels_bg[i]) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.member_panels_frame[i], ui); + // Panel Text + Text::new(&self.localized_strings.get("hud.group.out_of_range")) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0) + .font_size(16) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.dead_txt[i], ui); + } + } + + if self.show.group_menu { + let selected = state.selected_member; + 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, 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)) + .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() + { + self.show.group_menu = false; + self.show.group = !self.show.group; + 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) + .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(&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) + .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(); + 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, 135.0], color::TRANSPARENT) + .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) + .thickness(5.0) + .rgba(0.33, 0.33, 0.33, 1.0) + .set(state.ids.scrollbar, ui); + // List member names + 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 + } 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], 5.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(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) + .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 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, 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 + .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_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(12)) + .set(state.ids.btn_accept, ui) + .was_clicked() + { + events.push(Event::Accept); + self.show.group_menu = true; + }; + // 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_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(12)) + .set(state.ids.btn_decline, ui) + .was_clicked() + { + events.push(Event::Decline); + }; + } + + events + } +} diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 240860dd6e..04f60f0b3a 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -59,14 +59,19 @@ 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_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", // Crafting Window - crafting_window: "voxygen.element.misc_bg.crafting", crafting_frame: "voxygen.element.misc_bg.crafting_frame", crafting_icon_bordered: "voxygen.element.icons.anvil", @@ -74,6 +79,9 @@ image_ids! { crafting_icon_hover: "voxygen.element.buttons.anvil_hover", 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", // Chat-Arrows chat_arrow: "voxygen.element.buttons.arrow_down", @@ -94,7 +102,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 +118,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 8a1a88c358..9c42f1163e 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; @@ -72,7 +74,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); @@ -83,6 +85,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); @@ -112,12 +115,18 @@ 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 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 @@ -174,6 +183,7 @@ widget_ids! { num_chunks, num_lights, num_figures, + num_particles, // Game Version version, @@ -212,6 +222,7 @@ widget_ids! { social_window, crafting_window, settings_window, + group_window, // Free look indicator free_look_txt, @@ -243,11 +254,15 @@ pub struct DebugInfo { pub num_shadow_chunks: u32, pub num_figures: u32, pub num_figures_visible: u32, + pub num_particles: u32, + pub num_particles_visible: u32, } pub struct HudInfo { pub is_aiming: bool, pub is_first_person: bool, + pub target_entity: Option, + pub selected_entity: Option<(specs::Entity, std::time::Instant)>, } pub enum Event { @@ -270,7 +285,11 @@ pub enum Event { ChangeGamma(f32), MapZoom(f64), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, + ChangeResolution([u16; 2]), + ChangeBitDepth(Option), + ChangeRefreshRate(Option), CrosshairTransp(f32), ChatTransp(f32), ChatCharName(bool), @@ -302,6 +321,12 @@ pub enum Event { ChangeAutoWalkBehavior(PressBehavior), ChangeStopAutoWalkOnInput(bool), CraftRecipe(String), + InviteMember(common::sync::Uid), + AcceptInvite, + DeclineInvite, + KickMember(common::sync::Uid), + LeaveGroup, + AssignLeader(common::sync::Uid), } // TODO: Are these the possible layouts we want? @@ -357,6 +382,8 @@ pub struct Show { bag: bool, social: bool, spell: bool, + group: bool, + group_menu: bool, esc_menu: bool, open_windows: Windows, map: bool, @@ -394,7 +421,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; } @@ -494,7 +520,7 @@ impl Show { } fn toggle_social(&mut self) { - self.social = !self.social; + self.social(!self.social); self.spell = false; } @@ -610,6 +636,8 @@ impl Hud { ui: true, social: false, spell: false, + group: false, + group_menu: false, mini_map: true, settings_tab: SettingsTab::Interface, social_tab: SocialTab::Online, @@ -1040,7 +1068,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(), @@ -1053,11 +1081,34 @@ impl Hud { &uids, ) .join() - .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead) - // Don't show outside a certain range - .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { - pos.0.distance_squared(player_pos) - < (if hpfl + .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_key(uid), + ) + }) + .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) + || info.selected_entity.map_or(false, |s| s.0 == *entity) + || *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) { @@ -1067,25 +1118,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); @@ -1102,6 +1168,7 @@ impl Hud { stats, energy, own_level, + in_group, &global_state.settings.gameplay, self.pulse, &self.voxygen_i18n, @@ -1455,6 +1522,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( @@ -1464,7 +1542,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); @@ -1847,11 +1925,23 @@ impl Hud { settings_window::Event::AdjustGamma(new_gamma) => { events.push(Event::ChangeGamma(new_gamma)); }, + settings_window::Event::ChangeRenderMode(new_render_mode) => { + events.push(Event::ChangeRenderMode(new_render_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)); }, - settings_window::Event::ChangeRenderMode(new_render_mode) => { - events.push(Event::ChangeRenderMode(new_render_mode)); + settings_window::Event::ToggleParticlesEnabled(particles_enabled) => { + events.push(Event::ToggleParticlesEnabled(particles_enabled)); }, settings_window::Event::ToggleFullscreen => { events.push(Event::ToggleFullscreen); @@ -1880,23 +1970,56 @@ impl Hud { // Social Window if self.show.social { - for event in Social::new( - &self.show, - client, - &self.imgs, - &self.fonts, - &self.voxygen_i18n, - ) - .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) - }, + 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, + info.selected_entity, + &self.rot_imgs, + tooltip_manager, + ) + .set(self.ids.social_window, ui_widgets) + { + match event { + social::Event::Close => { + self.show.social(false); + self.force_ungrab = true; + }, + social::Event::ChangeSocialTab(social_tab) => { + self.show.open_social_tab(social_tab) + }, + social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), + } } } } + // Group Window + for event in Group::new( + &mut self.show, + client, + &global_state.settings, + &self.imgs, + &self.fonts, + &self.voxygen_i18n, + self.pulse, + &global_state, + ) + .set(self.ids.group_window, ui_widgets) + { + match event { + group::Event::Accept => events.push(Event::AcceptInvite), + group::Event::Decline => events.push(Event::DeclineInvite), + group::Event::Kick(uid) => events.push(Event::KickMember(uid)), + group::Event::LeaveGroup => events.push(Event::LeaveGroup), + group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), + } + } // Spellbook if self.show.spell { diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index 1c08691740..e5793c5bdc 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, @@ -42,6 +42,7 @@ widget_ids! { level_skull, health_bar, health_bar_bg, + health_txt, mana_bar, health_bar_fg, } @@ -56,11 +57,13 @@ 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, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, + #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -73,6 +76,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 +89,7 @@ impl<'a> Overhead<'a> { stats, energy, own_level, + in_group, settings, pulse, voxygen_i18n, @@ -104,15 +109,26 @@ 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: // - 1 for level: either Text or Image // - 3 for HP + fg + bg + // - 1 for HP text + // - If there's mana + // - 1 Rect::new for mana + // // If there's a speech bubble // - 2 Text::new for speech bubble // - 1 Image::new for icon // - 10 Image::new for speech bubble (9-slice + tail) - // If there's mana - // - 1 Rect::new for man - 6 + if self.bubble.is_some() { 13 } else { 0 } + if self.energy.is_some() { 1 } else { 0 } + 2 + if self.bubble.is_some() { 13 } else { 0 } + + if f64::from(self.stats.health.current()) / f64::from(self.stats.health.maximum()) + < 1.0 + { + 5 + if self.energy.is_some() { 1 } else { 0 } + } else { + 0 + } } } @@ -136,19 +152,35 @@ 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 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.abs() > 99.9 { 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) - .color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) - .x_y(0.0, MANA_BAR_Y + 50.0) + .font_size(font_size) + .color(if self.in_group { + GROUP_MEMBER + } else { + DEFAULT_NPC + }) + .x_y(0.0, name_y + 1.0) + .parent(id) .set(state.ids.name, ui); // Speech bubble @@ -162,7 +194,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); @@ -300,101 +332,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/settings_window.rs b/voxygen/src/hud/settings_window.rs index c14bb362c5..d9c849b1cc 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -18,6 +18,10 @@ use conrod_core::{ }; use core::convert::TryFrom; +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! { @@ -115,6 +119,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, lighting_mode_text, @@ -244,8 +259,12 @@ pub enum Event { AdjustLodDetail(u32), AdjustGamma(f32), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, ChangeRenderMode(Box), + ChangeResolution([u16; 2]), + ChangeBitDepth(Option), + ChangeRefreshRate(Option), AdjustMusicVolume(f32), AdjustSfxVolume(f32), ChangeAudioDevice(String), @@ -1200,6 +1219,10 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.chat_char_name_text, ui); + // TODO Show account name in chat + + // TODO 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) @@ -2202,11 +2225,191 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.shadow_mode_map_resolution_value, ui); } + // 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.shadow_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)); + } + + // 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.shadow_mode_list, 8.0) + .down_from(state.ids.resolution, 8.0) .color(TEXT_COLOR) .set(state.ids.fullscreen_label, ui); diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 6fdb6cdbd5..907ac4a5af 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; @@ -1153,15 +1157,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 @@ -1181,11 +1184,22 @@ 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)) @@ -1198,12 +1212,6 @@ impl<'a> Widget for Skillbar<'a> { .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 - ); 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 1bf7164bbe..78a9ef46c4 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -1,33 +1,60 @@ -use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, 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::{comp::group, 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_align, + title, + bg, + icon, scrollbar, + online_align, + online_tab, + 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, } } +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)>, +} + pub enum SocialTab { Online, Friends, @@ -41,25 +68,35 @@ pub struct Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + 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, + 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, + tooltip_manager, + selected_entity, common: widget::CommonBuilder::default(), } } @@ -67,223 +104,467 @@ impl<'a> Social<'a> { pub enum Event { Close, + Invite(Uid), ChangeSocialTab(SocialTab), } 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, + } + } #[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(); + 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); - Image::new(self.imgs.window_3) - .top_left_with_margins_on(ui.window, 200.0, 25.0) + // Window frame and BG + 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, + 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) + .bottom_left_with_margins_on(ui.window, 308.0, pos) .color(Some(UI_MAIN)) - .w_h(103.0 * 4.0, 122.0 * 4.0) - .set(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(ids.social_frame, 0.0, 0.0) - .set(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); } // 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(ids.social_frame, 6.0) + .middle_of(state.ids.title_align) .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(14)) + .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) - .set(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(ids.social_frame, 8.0 * 4.0) - .set(ids.align, ui); - // Content Alignment - Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT) - .middle_of(ids.frame) - .scroll_kids() - .scroll_kids_vertically() - .set(ids.content_align, ui); - Scrollbar::y_axis(ids.content_align) - .thickness(5.0) - .rgba(0.33, 0.33, 0.33, 1.0) - .set(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) - .color(Some(UI_MAIN)) - .set(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_online, + _ => 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) + .image_color(match &self.show.social_tab { + SocialTab::Online => UI_MAIN, + _ => Color::Rgba(1.0, 1.0, 1.0, 0.6), }) - .press_image(if let SocialTab::Online = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button_press - }) - .top_left_with_margins_on(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) - .color(UI_MAIN) - .label_color(TEXT_COLOR) - .set(ids.online_tab, ui) + .top_right_with_margins_on(state.ids.frame, 50.0, -27.0) + .set(state.ids.online_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Online)); } - - // 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 count = players.clone().count(); - if ids.player_names.len() < count { - ids.update(|ids| { - 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(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)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(ids.player_names[i], ui); - } - } - - // Friends Tab - - if Button::image(if let SocialTab::Friends = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button + // 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 * 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 + .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(if let SocialTab::Friends = self.show.social_tab { - self.imgs.social_button_pressed - } else { - self.imgs.social_button + .press_image(match &self.show.social_tab { + SocialTab::Friends => self.imgs.social_tab_act, + _ => self.imgs.social_tab_inact_press, }) - .right_from(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) - .color(UI_MAIN) - .label_color(TEXT_COLOR_3) - .set(ids.friends_tab, ui) + .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)); } - - // Contents - - if let SocialTab::Friends = self.show.social_tab { - Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(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); - } - - // 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(ids.friends_tab, 0.0) - .label(&self.localized_strings.get("hud.social.faction")) - .parent(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) - .was_clicked() + // 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)); } - - // Contents - - if let SocialTab::Faction = self.show.social_tab { - Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(ids.content_align) - .font_size(self.fonts.cyri.scale(18)) + // 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, 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, 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, 346.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(Color::Rgba(0.79, 1.09, 1.09, 0.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) - .color(TEXT_COLOR_3) - .set(ids.faction_test, ui); - } + .font_size(self.fonts.cyri.scale(14)) + .color(TEXT_COLOR) + .set(state.ids.online_txt, ui); + 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)) + .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 + // 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 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_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 + }; + 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 { + 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) + }; + 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(1.0)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_color(TEXT_COLOR) + .set(state.ids.player_names[i], ui); + // Player Levels + Button::image(if !selected { + self.imgs.nothing + } else { + 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 { + 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]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| s.selected_uid = Some((uid, Instant::now()))); + } + } + + // Invite Button + 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) + }) + }) + .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_to_invite.is_some() { + self.imgs.button_hover + } else { + self.imgs.button + }) + .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_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected_to_invite.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .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); + + 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_to_invite { + events.push(Event::Invite(uid)); + state.update(|s| { + s.selected_uid = None; + }); + } + } + } // End of Online Tab events } 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/"); 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/mesh/segment.rs b/voxygen/src/mesh/segment.rs index 091f8e0fb3..0ed2aaad8d 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -3,7 +3,10 @@ use crate::{ greedy::{self, GreedyConfig, GreedyMesh}, MeshGen, Meshable, }, - render::{self, FigurePipeline, Mesh, ShadowPipeline, SpritePipeline, TerrainPipeline}, + render::{ + self, FigurePipeline, Mesh, ParticlePipeline, ShadowPipeline, SpritePipeline, + TerrainPipeline, + }, }; use common::{ figure::Cell, @@ -13,6 +16,7 @@ use vek::*; type SpriteVertex = ::Vertex; type TerrainVertex = ::Vertex; +type ParticleVertex = ::Vertex; impl<'a: 'b, 'b, V: 'a> Meshable> for V where @@ -27,7 +31,6 @@ where type Supplement = (&'b mut GreedyMesh<'a>, Vec3, Vec3); type TranslucentPipeline = FigurePipeline; - #[allow(clippy::needless_range_loop)] // TODO: Pending review in #587 #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 fn generate_mesh( self, @@ -122,7 +125,6 @@ where type Supplement = (&'b mut GreedyMesh<'a>, bool); type TranslucentPipeline = SpritePipeline; - #[allow(clippy::needless_range_loop)] // TODO: Pending review in #587 #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 fn generate_mesh( self, @@ -201,6 +203,98 @@ where (opaque_mesh, Mesh::new(), Mesh::new(), ()) } } + +impl<'a: 'b, 'b, V: 'a> Meshable> 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 Result = (); + type ShadowPipeline = ShadowPipeline; + type Supplement = &'b mut GreedyMesh<'a>; + type TranslucentPipeline = ParticlePipeline; + + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + fn generate_mesh( + self, + greedy: Self::Supplement, + ) -> MeshGen, Self> { + let max_size = greedy.max_size(); + // NOTE: Required because we steal two bits from the normal in the shadow uint + // in order to store the bone index. The two bits are instead taken out + // of the atlas coordinates, which is why we "only" allow 1 << 15 per + // coordinate instead of 1 << 16. + assert!(max_size.width.max(max_size.height) < 1 << 16); + + let greedy_size = Vec3::new( + (self.upper_bound().x - self.lower_bound().x + 1) as usize, + (self.upper_bound().y - self.lower_bound().y + 1) as usize, + (self.upper_bound().z - self.lower_bound().z + 1) as usize, + ); + assert!( + greedy_size.x <= 16 && greedy_size.y <= 16 && greedy_size.z <= 64, + "Sprite size out of bounds: {:?} ≤ (15, 15, 63)", + greedy_size - 1 + ); + let greedy_size_cross = greedy_size; + let draw_delta = Vec3::new( + self.lower_bound().x, + self.lower_bound().y, + self.lower_bound().z, + ); + + let get_light = |vol: &mut V, pos: Vec3| { + if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) { + 1.0 + } else { + 0.0 + } + }; + let get_color = |vol: &mut V, pos: Vec3| { + vol.get(pos) + .ok() + .and_then(|vox| vox.get_color()) + .unwrap_or(Rgb::zero()) + }; + let get_opacity = + |vol: &mut V, pos: Vec3| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true); + let should_draw = |vol: &mut V, pos: Vec3, delta: Vec3, uv| { + should_draw_greedy(pos, delta, uv, |vox| { + vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty()) + }) + }; + let create_opaque = |_atlas_pos, pos: Vec3, norm| ParticleVertex::new(pos, norm); + + let mut opaque_mesh = Mesh::new(); + let _bounds = greedy.push(GreedyConfig { + data: self, + draw_delta, + greedy_size, + greedy_size_cross, + get_light, + get_color, + get_opacity, + should_draw, + push_quad: |atlas_origin, dim, origin, draw_dim, norm, meta: &()| { + opaque_mesh.push_quad(greedy::create_quad( + atlas_origin, + dim, + origin, + draw_dim, + norm, + meta, + |atlas_pos, pos, norm, &_meta| create_opaque(atlas_pos, pos, norm), + )); + }, + }); + + (opaque_mesh, Mesh::new(), Mesh::new(), ()) + } +} + fn should_draw_greedy( pos: Vec3, delta: Vec3, diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index b8bd57f66b..ea77ff2520 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -22,6 +22,7 @@ pub use self::{ }, fluid::FluidPipeline, lod_terrain::{Locals as LodTerrainLocals, LodData, LodTerrainPipeline}, + particle::{Instance as ParticleInstance, ParticlePipeline}, postprocess::{ create_mesh as create_pp_mesh, Locals as PostProcessLocals, PostProcessPipeline, }, diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index ea7ba0600c..db6c64ed8b 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -1,6 +1,7 @@ pub mod figure; pub mod fluid; pub mod lod_terrain; +pub mod particle; pub mod postprocess; pub mod shadow; pub mod skybox; @@ -177,6 +178,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 new file mode 100644 index 0000000000..32a2bb45d3 --- /dev/null +++ b/voxygen/src/render/pipelines/particle.rs @@ -0,0 +1,122 @@ +use super::{ + super::{Pipeline, TgtColorFmt, TgtDepthStencilFmt}, + shadow, Globals, Light, Shadow, +}; +use gfx::{ + self, gfx_defines, gfx_impl_struct_meta, gfx_pipeline, gfx_pipeline_inner, + gfx_vertex_struct_meta, state::ColorMask, +}; +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 { + // 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 + // 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. + // 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 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 { + vbuf: gfx::VertexBuffer = (), + ibuf: gfx::InstanceBuffer = (), + + globals: gfx::ConstantBuffer = "u_globals", + lights: gfx::ConstantBuffer = "u_lights", + shadows: gfx::ConstantBuffer = "u_shadows", + + point_shadow_maps: gfx::TextureSampler = "t_point_shadow_maps", + directed_shadow_maps: gfx::TextureSampler = "t_directed_shadow_maps", + + alt: gfx::TextureSampler<[f32; 2]> = "t_alt", + horizon: gfx::TextureSampler<[f32; 4]> = "t_horizon", + + noise: gfx::TextureSampler = "t_noise", + + // Shadow stuff + light_shadows: gfx::ConstantBuffer = "u_light_shadows", + + tgt_color: gfx::BlendTarget = ("tgt_color", ColorMask::all(), gfx::preset::blend::ALPHA), + tgt_depth_stencil: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, + // 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) -> 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(), + norm_ao: norm_bits, + } + } +} + +pub enum ParticleMode { + CampfireSmoke = 0, + CampfireFire = 1, + GunPowderSpark = 2, + Shrapnel = 3, +} + +impl ParticleMode { + pub fn into_uint(self) -> u32 { self as u32 } +} + +impl Instance { + 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: rand::thread_rng().gen(), + inst_mode: inst_mode as i32, + inst_pos: inst_pos.into_array(), + } + } +} + +impl Default for Instance { + fn default() -> Self { Self::new(0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } +} + +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 a1b19bc47f..dccac0f06f 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -5,8 +5,8 @@ use super::{ mesh::Mesh, model::{DynamicModel, Model}, pipelines::{ - figure, fluid, lod_terrain, postprocess, shadow, skybox, sprite, terrain, ui, GlobalModel, - Globals, + figure, fluid, lod_terrain, particle, postprocess, shadow, skybox, sprite, terrain, ui, + GlobalModel, Globals, }, texture::Texture, AaMode, CloudMode, FilterMethod, FluidMode, LightingMode, Pipeline, RenderError, RenderMode, @@ -131,6 +131,7 @@ pub struct Renderer { terrain_pipeline: GfxPipeline>, fluid_pipeline: GfxPipeline>, sprite_pipeline: GfxPipeline>, + particle_pipeline: GfxPipeline>, ui_pipeline: GfxPipeline>, lod_terrain_pipeline: GfxPipeline>, postprocess_pipeline: GfxPipeline>, @@ -179,6 +180,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, lod_terrain_pipeline, postprocess_pipeline, @@ -266,6 +268,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, lod_terrain_pipeline, postprocess_pipeline, @@ -729,6 +732,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, lod_terrain_pipeline, postprocess_pipeline, @@ -742,6 +746,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.lod_terrain_pipeline = lod_terrain_pipeline; self.postprocess_pipeline = postprocess_pipeline; @@ -1519,6 +1524,60 @@ impl Renderer { ); } + /// Queue the rendering of the provided particle in the upcoming frame. + pub fn render_particles( + &mut self, + model: &Model, + global: &GlobalModel, + instances: &Instances, + lod: &lod_terrain::LodData, + ) { + let (point_shadow_maps, directed_shadow_maps) = + if let Some(shadow_map) = &mut self.shadow_map { + ( + ( + shadow_map.point_res.clone(), + shadow_map.point_sampler.clone(), + ), + ( + shadow_map.directed_res.clone(), + shadow_map.directed_sampler.clone(), + ), + ) + } else { + ( + (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()), + (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()), + ) + }; + + 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: global.globals.buf.clone(), + lights: global.lights.buf.clone(), + shadows: global.shadows.buf.clone(), + light_shadows: global.shadow_mats.buf.clone(), + point_shadow_maps, + directed_shadow_maps, + noise: (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()), + alt: (lod.alt.srv.clone(), lod.alt.sampler.clone()), + horizon: (lod.horizon.srv.clone(), lod.horizon.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, @@ -1604,6 +1663,7 @@ fn create_pipelines( GfxPipeline>, GfxPipeline>, GfxPipeline>, + GfxPipeline>, GfxPipeline>, GfxPipeline>, GfxPipeline>, @@ -1805,6 +1865,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, @@ -1951,6 +2023,7 @@ fn create_pipelines( terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, lod_terrain_pipeline, postprocess_pipeline, diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index be3c600305..a0fa8af2be 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -24,11 +24,12 @@ 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, pub cam_pos: Vec3, + pub cam_dir: Vec3, } pub struct Camera { @@ -68,6 +69,7 @@ impl Camera { view_mat: Mat4::identity(), proj_mat: Mat4::identity(), cam_pos: Vec3::zero(), + cam_dir: Vec3::unit_y(), }, frustum: Frustum::from_modelview_projection(Mat4::identity().into_col_arrays()), } @@ -112,11 +114,13 @@ impl Camera { * Mat4::translation_3d(-self.focus.map(|e| e.trunc()))) .into_col_arrays(), ); + + self.dependents.cam_dir = Vec3::from(self.dependents.view_mat.inverted() * -Vec4::unit_z()); } pub fn frustum(&self) -> &Frustum { &self.frustum } - 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. diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index 24a1798f33..e588dfe24e 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -645,6 +645,11 @@ impl FigureModelCache { body.body_type, |segment, offset| generate_mesh(segment, offset), )), + Some(biped_large_center_spec.mesh_jaw( + body.species, + body.body_type, + |segment, offset| generate_mesh(segment, offset), + )), Some(biped_large_center_spec.mesh_torso_upper( body.species, body.body_type, @@ -655,11 +660,21 @@ impl FigureModelCache { body.body_type, |segment, offset| generate_mesh(segment, offset), )), + Some(biped_large_center_spec.mesh_tail( + body.species, + body.body_type, + |segment, offset| generate_mesh(segment, offset), + )), Some(biped_large_center_spec.mesh_main( body.species, body.body_type, |segment, offset| generate_mesh(segment, offset), )), + Some(biped_large_center_spec.mesh_second( + body.species, + body.body_type, + |segment, offset| generate_mesh(segment, offset), + )), Some(biped_large_lateral_spec.mesh_shoulder_l( body.species, body.body_type, @@ -701,9 +716,6 @@ impl FigureModelCache { |segment, offset| generate_mesh(segment, offset), )), None, - None, - None, - None, ] }, Body::Golem(body) => { diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 57bc888455..be905d731c 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -2477,9 +2477,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 { @@ -2550,6 +2553,27 @@ impl BipedLargeCenterSpec { generate_mesh(center, Vec3::from(spec.head.offset)) } + pub fn mesh_jaw( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(Segment, Vec3) -> BoneMeshes, + ) -> BoneMeshes { + 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(center, Vec3::from(spec.jaw.offset)) + } + pub fn mesh_torso_upper( &self, species: BLSpecies, @@ -2592,6 +2616,27 @@ impl BipedLargeCenterSpec { generate_mesh(center, Vec3::from(spec.torso_lower.offset)) } + pub fn mesh_tail( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(Segment, Vec3) -> BoneMeshes, + ) -> BoneMeshes { + 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(center, Vec3::from(spec.tail.offset)) + } + pub fn mesh_main( &self, species: BLSpecies, @@ -2612,6 +2657,27 @@ impl BipedLargeCenterSpec { generate_mesh(center, Vec3::from(spec.main.offset)) } + + pub fn mesh_second( + &self, + species: BLSpecies, + body_type: BLBodyType, + generate_mesh: impl FnOnce(Segment, Vec3) -> BoneMeshes, + ) -> BoneMeshes { + 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(center, Vec3::from(spec.second.offset)) + } } impl BipedLargeLateralSpec { pub fn load_watched(indicator: &mut ReloadIndicator) -> Arc { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 4b0e1b703d..98ff990064 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -26,8 +26,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, @@ -438,6 +438,7 @@ impl FigureMgr { proj_mat: _, view_mat: _, cam_pos, + .. } = camera.dependents(); let cam_pos = math::Vec3::from(cam_pos); let ray_direction = math::Vec3::from(ray_direction); @@ -499,6 +500,7 @@ impl FigureMgr { physics, stats, loadout, + item, ), ) in ( &ecs.entities(), @@ -512,6 +514,7 @@ impl FigureMgr { &ecs.read_storage::(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() .enumerate() @@ -612,7 +615,13 @@ impl FigureMgr { (c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32 }) }) - .unwrap_or(vek::Rgba::broadcast(1.0)); + .unwrap_or(vek::Rgba::broadcast(1.0)) + // Highlight targeted collectible entities + * if item.is_some() && scene_data.target_entity.map_or(false, |e| e == entity) { + vek::Rgba::new(2.0, 2.0, 2.0, 1.0) + } else { + vek::Rgba::one() + }; let scale = scale.map(|s| s.0).unwrap_or(1.0); @@ -671,7 +680,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::default(), ( active_tool_kind.clone(), @@ -684,7 +693,7 @@ impl FigureMgr { skeleton_attr, ), // Running - (true, true, _) => anim::character::RunAnimation::update_skeleton( + (true, true, false) => anim::character::RunAnimation::update_skeleton( &CharacterSkeleton::default(), ( active_tool_kind.clone(), @@ -714,7 +723,7 @@ impl FigureMgr { skeleton_attr, ), // Swim - (false, _, true) => anim::character::SwimAnimation::update_skeleton( + (_, _, true) => anim::character::SwimAnimation::update_skeleton( &CharacterSkeleton::default(), ( active_tool_kind.clone(), @@ -723,6 +732,7 @@ impl FigureMgr { ori, state.last_ori, time, + state.avg_vel, ), state.state_time, &mut state_animation_rate, @@ -806,6 +816,15 @@ impl FigureMgr { ) } }, + CharacterState::Sneak { .. } => { + anim::character::SneakAnimation::update_skeleton( + &CharacterSkeleton::default(), + (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, @@ -900,13 +919,23 @@ impl FigureMgr { ) }, CharacterState::Wielding { .. } => { - 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, - ) + 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( diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 4f8bda8254..31932eaadd 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -2,13 +2,15 @@ pub mod camera; pub mod figure; pub mod lod; pub mod math; +pub mod particle; pub mod simple; pub mod terrain; -use self::{ +pub use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, lod::Lod, + particle::ParticleMgr, terrain::Terrain, }; use crate::{ @@ -25,7 +27,8 @@ use anim::character::SkeletonAttr; use client::Client; use common::{ comp, - state::State, + outcome::Outcome, + state::{DeltaTime, State}, terrain::{BlockKind, TerrainChunk}, vol::ReadVol, }; @@ -55,6 +58,12 @@ const RUNNING_THRESHOLD: f32 = 0.7; /// is_daylight, array of active lights. pub type LightData<'a> = (bool, &'a [Light]); +struct EventLight { + light: Light, + timeout: f32, + fadeout: fn(f32) -> f32, +} + struct Skybox { model: Model, locals: Consts, @@ -69,6 +78,7 @@ pub struct Scene { data: GlobalModel, camera: Camera, camera_input_state: Vec2, + event_lights: Vec, skybox: Skybox, postprocess: PostProcess, @@ -82,6 +92,7 @@ pub struct Scene { select_pos: Option>, light_data: Vec, + particle_mgr: ParticleMgr, figure_mgr: FigureMgr, sfx_mgr: SfxMgr, music_mgr: MusicMgr, @@ -90,6 +101,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, @@ -97,6 +109,7 @@ pub struct SceneData<'a> { pub gamma: f32, pub mouse_smoothing: bool, pub sprite_render_distance: f32, + pub particles_enabled: bool, pub figure_lod_render_distance: f32, pub is_aiming: bool, } @@ -261,6 +274,7 @@ impl Scene { }, 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(), @@ -278,7 +292,7 @@ impl Scene { map_bounds: client.world_map.2, select_pos: None, light_data: Vec::new(), - + particle_mgr: ParticleMgr::new(renderer), figure_mgr: FigureMgr::new(renderer), sfx_mgr: SfxMgr::new(), music_mgr: MusicMgr::new(), @@ -297,6 +311,9 @@ impl Scene { /// Get a reference to the scene's lights. pub fn lights(&self) -> &Vec { &self.light_data } + /// 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 } @@ -346,6 +363,25 @@ impl Scene { } } + 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), + timeout: 0.5, + fadeout: |timeout| timeout * 2.0, + }), + Outcome::ProjectileShot { .. } => {}, + } + } + /// Maintain data such as GPU constant buffers, models, etc. To be called /// once per tick. pub fn maintain( @@ -435,6 +471,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); // Update chunk loaded distance smoothly for nice shader fog @@ -481,7 +518,12 @@ impl Scene { light_anim.col, light_anim.strength, ) - }), + }) + .chain( + self.event_lights + .iter() + .map(|el| el.light.with_strength((el.fadeout)(el.timeout))), + ), ); lights.sort_by_key(|light| light.get_pos().distance_squared(player_pos) as i32); lights.truncate(MAX_LIGHT_COUNT); @@ -489,6 +531,13 @@ impl Scene { .update_consts(&mut self.data.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::(), @@ -890,9 +939,16 @@ impl Scene { // Remove unused figures. self.figure_mgr.clean(scene_data.tick); + // Maintain the particles. + self.particle_mgr.maintain(renderer, &scene_data); + // Maintain audio - self.sfx_mgr - .maintain(audio, scene_data.state, scene_data.player_entity); + self.sfx_mgr.maintain( + audio, + scene_data.state, + scene_data.player_entity, + &self.camera, + ); self.music_mgr.maintain(audio, scene_data.state); } @@ -923,11 +979,11 @@ impl Scene { // Render terrain shadows. self.terrain - .render_shadows(renderer, &global, light_data, focus_pos); + .render_shadows(renderer, global, light_data, focus_pos); // Render figure shadows. self.figure_mgr - .render_shadows(renderer, state, tick, &global, light_data, camera_data); + .render_shadows(renderer, state, tick, global, light_data, camera_data); if is_daylight { // Flush shadows. @@ -941,31 +997,34 @@ impl Scene { state, player_entity, tick, - &global, + global, lod, camera_data, ); // Render terrain and figures. - self.terrain.render(renderer, &global, lod, focus_pos); + self.terrain.render(renderer, global, lod, focus_pos); self.figure_mgr.render( renderer, state, player_entity, tick, - &global, + global, lod, camera_data, ); - self.lod.render(renderer, &global); + self.lod.render(renderer, global); + + // Render particle effects. + self.particle_mgr.render(renderer, scene_data, global, lod); // Render the skybox. - renderer.render_skybox(&self.skybox.model, &global, &self.skybox.locals, lod); + renderer.render_skybox(&self.skybox.model, global, &self.skybox.locals, lod); self.terrain.render_translucent( renderer, - &global, + global, lod, focus_pos, cam_pos, diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs new file mode 100644 index 0000000000..79c6435bbd --- /dev/null +++ b/voxygen/src/scene/particle.rs @@ -0,0 +1,389 @@ +use super::SceneData; +use crate::{ + mesh::{greedy::GreedyMesh, Meshable}, + render::{ + pipelines::particle::ParticleMode, GlobalModel, Instances, LodData, Model, + ParticleInstance, ParticlePipeline, Renderer, + }, +}; +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; +use vek::*; + +pub struct ParticleMgr { + /// 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>, +} + +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 handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { + let time = scene_data.state.get_time(); + let mut rng = rand::thread_rng(); + + match outcome { + Outcome::Explosion { pos, power } => { + for _ in 0..150 { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::Shrapnel, + *pos, + )); + } + for _ in 0..200 { + 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 { .. } => {}, + } + } + + pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + if scene_data.particles_enabled { + // update timings + self.scheduler.maintain(scene_data.state.get_time()); + + // remove dead Particle + 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); + } else { + // remove all particle lifespans + self.particles.clear(); + + // remove all timings + self.scheduler.clear(); + } + + self.upload_particles(renderer); + } + + fn maintain_body_particles(&mut self, scene_data: &SceneData) { + let ecs = scene_data.state.ecs(); + for (_i, (_entity, body, pos)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + match body { + Body::Object(object::Body::CampfireLit) => { + self.maintain_campfirelit_particles(scene_data, pos) + }, + Body::Object(object::Body::BoltFire) => { + self.maintain_boltfire_particles(scene_data, pos) + }, + Body::Object(object::Body::BoltFireBig) => { + self.maintain_boltfirebig_particles(scene_data, pos) + }, + Body::Object(object::Body::Bomb) => self.maintain_bomb_particles(scene_data, pos), + _ => {}, + } + } + } + + fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) { + let time = scene_data.state.get_time(); + + 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(10), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } + } + + fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) { + let time = scene_data.state.get_time(); + + 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(); + + // fire + 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 + 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(); + + 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(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(); + + for (_i, (_entity, pos, character_state)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + if let CharacterState::Boost(_) = character_state { + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_secs(15), + time, + 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, + scene_data: &SceneData, + global: &GlobalModel, + lod: &LodData, + ) { + if scene_data.particles_enabled { + let model = &self + .model_cache + .get(DEFAULT_MODEL_KEY) + .expect("Expected particle model in cache"); + + renderer.render_particles(model, global, &self.instances, lod); + } + } + + 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 { + let empty_vec = Vec::new(); + + renderer + .create_instances(&empty_vec) + .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(DEFAULT_MODEL_KEY).or_insert_with(|| { + let vox = assets::load_expect::(DEFAULT_MODEL_KEY); + + // NOTE: If we add texturing we may eventually try to share it among all + // particles in a single atlas. + let max_texture_size = renderer.max_texture_size(); + let max_size = + guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size)); + let mut greedy = GreedyMesh::new(max_size); + + let mesh = Meshable::::generate_mesh( + Segment::from(vox.as_ref()), + &mut greedy, + ) + .0; + + // NOTE: Ignoring coloring / lighting for now. + drop(greedy); + + renderer + .create_model(&mesh) + .expect("Failed to create particle model") + }); + + model_cache +} + +/// Accumulates heartbeats to be consumed on the next tick. +struct HeartbeatScheduler { + /// Duration = Heartbeat Frequency/Intervals + /// f64 = Last update time + /// u8 = number of heartbeats since last update + /// - 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 etc. + 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, now: f64) { + self.last_known_time = now; + + 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; + } + } + + /// 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 { + let last_known_time = self.last_known_time; + + self.timers + .entry(frequency) + .or_insert_with(|| (last_known_time, 0)) + .1 + } + + pub fn clear(&mut self) { self.timers.clear() } +} + +struct Particle { + alive_until: f64, // created_at + lifespan + instance: ParticleInstance, +} + +impl Particle { + fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3) -> Self { + Particle { + alive_until: time + lifespan.as_secs_f64(), + instance: ParticleInstance::new(time, mode, pos), + } + } +} diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index b5c1abf641..d33426e6b0 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -228,6 +228,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); const VD: f32 = 115.0; // View Distance const TIME: f64 = 10.0 * 60.0 * 60.0; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index fa8fc7bb97..f9b4588099 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -21,6 +21,7 @@ use common::{ }, event::EventBus, msg::ClientState, + outcome::Outcome, terrain::{Block, BlockKind}, util::Dir, vol::ReadVol, @@ -52,6 +53,8 @@ pub struct SessionState { free_look: bool, auto_walk: bool, is_aiming: bool, + target_entity: Option, + selected_entity: Option<(specs::Entity, std::time::Instant)>, } /// Represents an active game session (i.e., the one being played). @@ -90,6 +93,8 @@ impl SessionState { free_look: false, auto_walk: false, is_aiming: false, + target_entity: None, + selected_entity: None, } } @@ -100,7 +105,12 @@ 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,6 +168,7 @@ impl SessionState { global_state.settings.graphics.view_distance = vd; global_state.settings.save_to_file_warn(); }, + client::Event::Outcome(outcome) => outcomes.push(outcome), } } @@ -209,24 +220,12 @@ 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 focus_pos = self.scene.camera().get_focus_pos(); let focus_off = focus_pos.map(|e| e.trunc()); let cam_pos = cam_pos + focus_off; - // 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 @@ -247,35 +246,11 @@ impl PlayState for SessionState { }; self.is_aiming = is_aiming; - let cam_dir: Vec3 = Vec3::from( - (view_mat/* * Mat4::translation_3d(-focus_off */).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 + self.target_entity = target_entity.map(|x| x.0); let can_build = self .client @@ -359,8 +334,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 => @@ -381,6 +359,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(); @@ -568,6 +555,24 @@ 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.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; @@ -652,10 +657,16 @@ 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) => { @@ -709,6 +720,9 @@ impl PlayState for SessionState { num_shadow_chunks: self.scene.terrain().shadow_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, }) } else { None @@ -727,6 +741,8 @@ impl PlayState for SessionState { self.scene.camera().get_mode(), camera::CameraMode::FirstPerson ), + target_entity: self.target_entity, + selected_entity: self.selected_entity, }, ); @@ -938,6 +954,37 @@ impl PlayState for SessionState { global_state.settings.graphics.render_mode = *new_render_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; @@ -949,6 +996,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 @@ -978,6 +1029,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::DeclineInvite => { + self.client.borrow_mut().decline_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); + }, } } @@ -986,6 +1055,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(), @@ -994,6 +1064,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, + particles_enabled: global_state.settings.graphics.particles_enabled, figure_lod_render_distance: global_state .settings .graphics @@ -1009,6 +1080,12 @@ impl PlayState for SessionState { &mut global_state.audio, &scene_data, ); + + // Process outcomes from client + for outcome in outcomes { + self.scene + .handle_outcome(&outcome, &scene_data, &mut global_state.audio); + } } } @@ -1040,6 +1117,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(), @@ -1048,6 +1126,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, + particles_enabled: settings.graphics.particles_enabled, is_aiming: self.is_aiming, }; self.scene.render( @@ -1062,3 +1141,108 @@ 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 +#[allow(clippy::type_complexity)] +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 = 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); + // 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)) + // 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 { + 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 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); + (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) +} diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 65915a5e09..b9db4381a7 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -134,7 +134,9 @@ 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::Sneak => KeyMouse::Key(VirtualKeyCode::LControl), //GameInput::WallLeap => MIDDLE_CLICK_KEY, GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G), GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F), @@ -169,6 +171,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::Y), + GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U), + GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I), } } } @@ -196,7 +201,9 @@ impl Default for ControlSettings { GameInput::Glide, GameInput::Climb, GameInput::ClimbDown, - GameInput::Swim, + GameInput::SwimUp, + GameInput::SwimDown, + GameInput::Sneak, //GameInput::WallLeap, GameInput::ToggleLantern, GameInput::Mount, @@ -234,6 +241,9 @@ impl Default for ControlSettings { GameInput::Slot9, 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)); @@ -302,7 +312,9 @@ pub mod con_settings { pub glide: Button, pub climb: Button, pub climb_down: Button, - pub swim: Button, + pub swimup: Button, + pub swimdown: Button, + pub sneak: Button, //pub wall_leap: Button, pub toggle_lantern: Button, pub mount: Button, @@ -392,7 +404,9 @@ 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), + sneak: Button::Simple(GilButton::Unknown), //wall_leap: Button::Simple(GilButton::Unknown), toggle_lantern: Button::Simple(GilButton::East), mount: Button::Simple(GilButton::North), @@ -597,11 +611,15 @@ impl Default for Log { pub struct GraphicsSettings { pub view_distance: u32, pub sprite_render_distance: u32, + pub particles_enabled: bool, pub figure_lod_render_distance: u32, pub max_fps: u32, pub fov: u16, pub gamma: f32, pub render_mode: RenderMode, + pub resolution: [u16; 2], + pub bit_depth: Option, + pub refresh_rate: Option, pub window_size: [u16; 2], pub fullscreen: bool, pub lod_detail: u32, @@ -612,11 +630,15 @@ impl Default for GraphicsSettings { Self { view_distance: 10, sprite_render_distance: 150, + particles_enabled: true, figure_lod_render_distance: 250, max_fps: 60, fov: 50, gamma: 1.0, render_mode: RenderMode::default(), + resolution: [1920, 1080], + bit_depth: None, + refresh_rate: None, window_size: [1920, 1080], fullscreen: false, lod_detail: 300, diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index a5791471fb..ca6915a272 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)] @@ -39,7 +41,9 @@ pub enum GameInput { Glide, Climb, ClimbDown, - Swim, + SwimUp, + SwimDown, + Sneak, //WallLeap, ToggleLantern, Mount, @@ -67,6 +71,9 @@ pub enum GameInput { FreeLook, AutoWalk, CycleCamera, + Select, + AcceptGroupInvite, + DeclineGroupInvite, } impl GameInput { @@ -85,7 +92,9 @@ 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::Sneak => "gameinput.sneak", //GameInput::WallLeap => "gameinput.wallleap", GameInput::ToggleLantern => "gameinput.togglelantern", GameInput::Mount => "gameinput.mount", @@ -123,6 +132,9 @@ impl GameInput { GameInput::Slot9 => "gameinput.slot9", GameInput::Slot10 => "gameinput.slot10", GameInput::SwapLoadout => "gameinput.swaploadout", + GameInput::Select => "gameinput.select", + GameInput::AcceptGroupInvite => "gameinput.acceptgroupinvite", + GameInput::DeclineGroupInvite => "gameinput.declinegroupinvite", } } @@ -141,7 +153,9 @@ impl GameInput { GameInput::Glide, GameInput::Climb, GameInput::ClimbDown, - GameInput::Swim, + GameInput::SwimUp, + GameInput::SwimDown, + GameInput::Sneak, GameInput::ToggleLantern, GameInput::Mount, GameInput::Enter, @@ -195,7 +209,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, @@ -596,7 +610,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)) } @@ -1042,34 +1061,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); 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",