Merge remote-tracking branch 'origin/master' into sharp/small-fixes

This commit is contained in:
Joshua Yanovski 2020-08-12 16:32:24 +02:00
commit 0ed801d540
146 changed files with 7414 additions and 1446 deletions

View File

@ -4,4 +4,7 @@ rustflags = [
]
[alias]
generate = "run --package tools --"
generate = "run --package tools --"
test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features"
server = "run --bin veloren-server-cli"

View File

@ -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

5
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -5,7 +5,7 @@ Item(
(
kind: Back("Short0"),
stats: (
protection: Normal(0.0),
protection: Normal(0.2),
),
)
),

View File

@ -0,0 +1,12 @@
Item(
name: "Green Blanket",
description: "Keeps your shoulders warm.",
kind: Armor(
(
kind: Back("Short1"),
stats: (
protection: Normal(0.1),
),
)
),
)

View File

@ -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),
),
)
),
)

View File

@ -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"),

View File

@ -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"),
]

View File

@ -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,
),
}
)

BIN
assets/voxygen/audio/sfx/explosion.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/group.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/group_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/group_press.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/social_tab_active.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/social_tab_inactive.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/enemybar_1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/enemybar_bg_1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/group_member_bg.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/group_member_frame.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/neck-0.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/icons/neck-1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/social_bg.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/social_frame.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/social_tab_active.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/social_tab_inactive.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/social_tab_online.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -388,5 +388,9 @@ Siła woli
"esc_menu.quit_game": "Opuść gre",
/// End Escape Menu Section
/// Koniec sekcji Menu pauzy
}
},
vector_map: {
},
)

View File

@ -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

View File

@ -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!",

View File

@ -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
Dexteri
Volon
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 !",
],
}
)
)

View File

@ -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: {
},
)

View File

@ -377,5 +377,9 @@ Veloren 半夜會特別暗。
"esc_menu.logout": "登出",
"esc_menu.quit_game": "退出遊戲",
/// End Escape Menu Section
}
},
vector_map: {
},
)

View File

@ -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",

View File

@ -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;
}

View File

@ -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) {

View File

@ -0,0 +1,86 @@
#version 330 core
#include <constants.glsl>
#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 <globals.glsl>
in vec3 f_pos;
flat in vec3 f_norm;
in vec3 f_col;
out vec4 tgt_color;
#include <sky.glsl>
#include <light.glsl>
#include <lod.glsl>
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));
}

View File

@ -0,0 +1,140 @@
#version 330 core
#include <constants.glsl>
#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 <globals.glsl>
#include <srgb.glsl>
#include <random.glsl>
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);
}

BIN
assets/voxygen/voxel/armor/back/short-1.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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"),

View File

@ -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
),
},
))

BIN
assets/voxygen/voxel/particle.vox (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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<String>,
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<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
// Pending invites that this client has sent out
pending_invites: HashSet<Uid>,
_network: Network,
participant: Option<Participant>,
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::<comp::Last<comp::CharacterState>>();
// Initialize `State`
let mut state = State::default();
// Client-only components
state
.ecs_mut()
.register::<comp::Last<comp::CharacterState>>();
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 * <f32 as FloatConst>::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 * <f32 as FloatConst>::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::<Vec<_>>(),
heights
.iter()
.copied()
.map(scale_height)
.collect::<Vec<_>>(),
)
};
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::<Vec<_>>(),
heights
.iter()
.copied()
.map(scale_height)
.collect::<Vec<_>>(),
)
};
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<i32>| {
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<i32>| {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Uid>().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<Uid, group::Role> { &self.group_members }
pub fn pending_invites(&self) -> &HashSet<Uid> { &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::<Uid>().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::<comp::CharacterState>()
.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::<Uid>(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<Uid> { 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::<Uid>(self.entity)
.read_component_copied::<Uid>(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::<Uid>(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::<Uid>().get(self.entity) {
if Some(*from) == self.uid() {
format!("To [{}]: {}", to_alias, message)
} else {
format!("From [{}]: {}", from_alias, message)

View File

@ -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"

View File

@ -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",

View File

@ -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<Self, IdvStorage<Self>>;
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug, Default)]

View File

@ -41,6 +41,7 @@ pub enum CharacterState {
Climb,
Sit,
Dance,
Sneak,
Glide,
GlideWield,
/// A basic blocking state

View File

@ -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<G> {
/// 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<S>(self, msg: S) -> ChatMsg
impl<G> ChatType<G> {
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G>
where
S: Into<String>,
{
ChatMsg {
GenericChatMsg {
chat_type: self,
message: msg.into(),
}
}
}
impl ChatType<String> {
pub fn server_msg<S>(self, msg: S) -> ServerMsg
where
S: Into<String>,
@ -106,12 +107,15 @@ impl ChatType {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMsg {
pub chat_type: ChatType,
pub struct GenericChatMsg<G> {
pub chat_type: ChatType<G>,
pub message: String,
}
impl ChatMsg {
pub type ChatMsg = GenericChatMsg<String>;
pub type UnresolvedChatMsg = GenericChatMsg<Group>;
impl<G> GenericChatMsg<G> {
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<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> {
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<Self>;
}
impl From<String> 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
///

View File

@ -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<Climb>,
pub swim: Input,
pub swimup: Input,
pub swimdown: Input,
pub move_dir: Vec2<f32>,
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;
}

528
common/src/comp/group.rs Normal file
View File

@ -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<Self, IdvStorage<Self>>;
}
pub struct Invite(pub specs::Entity);
impl Component for Invite {
type Storage = IdvStorage<Self>;
}
// 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<Self>;
}
#[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<E> {
// :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<Uid> everywhere
// Also note when the same notification is sent to multiple destinations the
// maping might be duplicated effort
impl<E> ChangeNotification<E> {
pub fn try_map<T>(self, f: impl Fn(E) -> Option<T>) -> Option<ChangeNotification<T>> {
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<GroupInfo>,
}
// 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<specs::Entity> {
(entities, alignments)
.join()
.filter_map(|(e, a)| {
matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e)
})
.collect::<Vec<_>>()
}
/// Returns list of current members of a group
pub fn members<'a>(
group: Group,
groups: impl Join<Type = &'a Group> + 'a,
entities: &'a specs::Entities,
alignments: &'a Alignments,
uids: &'a Uids,
) -> impl Iterator<Item = (specs::Entity, Role)> + '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<specs::Entity>),
) {
// 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<specs::Entity>),
) {
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<specs::Entity>),
) {
// 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<specs::Entity>),
) {
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<specs::Entity>),
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::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::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::<Vec<_>>();
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<specs::Entity>),
) {
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 => {},
});
}
}

View File

@ -261,6 +261,7 @@ impl Tool {
col: (0.85, 0.5, 0.11).into(),
..Default::default()
}),
projectile_gravity: None,
},
BasicRanged {

View File

@ -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,

View File

@ -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<f32> { &*self.0 }
}
impl Component for Ori {
type Storage = IdvStorage<Self>;
}

View File

@ -25,6 +25,7 @@ pub enum ServerEvent {
pos: Vec3<f32>,
power: f32,
owner: Option<Uid>,
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<i32>),
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<E> {

View File

@ -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;

View File

@ -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<comp::LightEmitter>),
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
Alignment(PhantomData<comp::Alignment>),
Group(PhantomData<comp::Group>),
MountState(PhantomData<comp::MountState>),
Mounting(PhantomData<comp::Mounting>),
Mass(PhantomData<comp::Mass>),
@ -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::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::Alignment(_) => sync::handle_remove::<comp::Alignment>(entity, world),
EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world),
EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),

View File

@ -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,
},
};

View File

@ -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<u8>, Vec<u8>); 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<EcsCompPacket>,
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<sync::Uid>),
// Indicate to the client that they are invited to join a group
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
},
// Indicate to the client that their sent invite was not invalid and is currently pending
InvitePending(sync::Uid),
// Note: this could potentially include all the failure cases such as inviting yourself in
// which case the `InvitePending` message could be removed and the client could consider their
// invite pending until they receive this message
// Indicate to the client the result of their invite
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
},
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
/// 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<Outcome>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

30
common/src/outcome.rs Normal file
View File

@ -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<f32>,
power: f32,
},
ProjectileShot {
pos: Vec3<f32>,
body: comp::Body,
vel: Vec3<f32>,
},
}
impl Outcome {
pub fn get_pos(&self) -> Option<Vec3<f32>> {
match self {
Outcome::Explosion { pos, .. } => Some(*pos),
Outcome::ProjectileShot { pos, .. } => Some(*pos),
}
}
}

View File

@ -123,7 +123,7 @@ impl State {
ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>();
ecs.register::<comp::Object>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Group>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();
@ -146,6 +146,7 @@ impl State {
ecs.register::<comp::Last<comp::Pos>>();
ecs.register::<comp::Last<comp::Vel>>();
ecs.register::<comp::Last<comp::Ori>>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Agent>();
ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>();
@ -156,8 +157,9 @@ impl State {
ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
ecs.register::<comp::Group>();
ecs.register::<comp::Faction>();
ecs.register::<comp::group::Invite>();
ecs.register::<comp::group::PendingInvites>();
// 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::<LocalEvent>::default());
// TODO: only register on the server
ecs.insert(EventBus::<ServerEvent>::default());
ecs.insert(EventBus::<LocalEvent>::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<C: Component + Clone>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).cloned()
pub fn read_component_copied<C: Component + Copy>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).copied()
}
/// Get a read-only reference to the storage of a particular component type.

View File

@ -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(),

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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
}
}

View File

@ -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()

View File

@ -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;

View File

@ -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<ServerEvent>>,
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::<f32>() - 0.5,
) * 0.1
- *bearing * 0.003
- if let Some(patrol_origin) = agent.patrol_origin {
Vec2::<f32>::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));
}
}
}
}

View File

@ -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),

View File

@ -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),
});
}

View File

@ -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)),
}
}

View File

@ -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<ServerEvent>>,
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);

View File

@ -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 {

View File

@ -77,6 +77,7 @@ impl<'a> System<'a> for Sys {
CharacterState::Idle { .. }
| CharacterState::Sit { .. }
| CharacterState::Dance { .. }
| CharacterState::Sneak { .. }
| CharacterState::Glide { .. }
| CharacterState::GlideWield { .. }
| CharacterState::Wielding { .. }

View File

@ -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::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(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::<comp::Pos>(target)
.read_component_copied::<comp::Pos>(target)
.is_some()
{
server
@ -463,9 +463,9 @@ fn handle_tp(
);
return;
};
if let Some(_pos) = server.state.read_component_cloned::<comp::Pos>(target) {
if let Some(_pos) = server.state.read_component_copied::<comp::Pos>(target) {
if let Some(player) = opt_player {
if let Some(pos) = server.state.read_component_cloned::<comp::Pos>(player) {
if let Some(pos) = server.state.read_component_copied::<comp::Pos>(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::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(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::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager =
state.ecs().write_resource::<comp::group::GroupManager>();
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::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(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::<comp::Pos>(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::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(pos) => {
ecs.read_resource::<EventBus<ServerEvent>>()
.emit_now(ServerEvent::Explosion {
pos: pos.0,
power,
owner: ecs.read_storage::<Uid>().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::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(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::<comp::Admin>(player)
.read_component_copied::<comp::Admin>(player)
.is_some()
{
ecs.write_storage::<comp::Admin>().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::<comp::Group>().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::<comp::Player>()
.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::<comp::Pos>(target);
let opt_player_pos = server.state.read_component_copied::<comp::Pos>(target);
let mut to_delete = vec![];
match opt_player_pos {

View File

@ -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<Item>,
) {
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::<Vec<Outcome>>()
.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)
}

View File

@ -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::<Stats>();
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::<Group>();
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::<Pos>();
let alignments = state.ecs().read_storage::<Alignment>();
let uids = state.ecs().read_storage::<Uid>();
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::<Vec<_>>();
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::<comp::Waypoint>(entity)
.read_component_copied::<comp::Waypoint>(entity)
.map(|wp| wp.get_pos())
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);
@ -217,11 +278,29 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
}
}
pub fn handle_explosion(server: &Server, pos: Vec3<f32>, power: f32, owner: Option<Uid>) {
pub fn handle_explosion(
server: &Server,
pos: Vec3<f32>,
power: f32,
owner: Option<Uid>,
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::<Vec<Outcome>>()
.push(Outcome::Explosion { pos, power });
let owner_entity = owner.and_then(|uid| {
ecs.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid.into())
});
let groups = ecs.read_storage::<comp::Group>();
for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Ori>(),
ecs.read_storage::<comp::CharacterState>().maybe(),
@ -233,9 +312,13 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, 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;

View File

@ -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::<Client>();
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::<sync::Uid>();
// 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::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>();
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::<PendingInvites>();
// 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::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
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::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
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::<GroupManager>();
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::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
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::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
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::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let alignments = state.ecs().read_storage::<comp::Alignment>();
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::<group::Group>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
// 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::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
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::<group::Group>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
// 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(),
));
}
},
}
},
}
}

View File

@ -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::<comp::Vel>(entity)
.read_component_copied::<comp::Vel>(entity)
.unwrap_or_default(),
state
.read_component_cloned::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(entity)
.unwrap_or_default(),
*kind,
));
@ -184,7 +185,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
state.read_storage::<comp::Pos>().get(entity)
{
let uid = state
.read_component_cloned(entity)
.read_component_copied(entity)
.expect("Expected player to have a UID");
if (
&state.read_storage::<comp::Alignment>(),
@ -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::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager = state
.ecs()
.write_resource::<comp::group::GroupManager>(
);
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::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(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::<comp::Pos>(entity)
.read_component_copied::<comp::Pos>(entity)
.unwrap_or_default(),
state
.read_component_cloned::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(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::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0;
let uid = state.read_component_cloned::<Uid>(entity);
let uid = state.read_component_copied::<Uid>(entity);
let mut new_entity = state
.create_object(Default::default(), match kind {

View File

@ -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)

View File

@ -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::<Client>().remove(entity);
let maybe_uid = state.read_component_cloned::<Uid>(entity);
let maybe_uid = state.read_component_copied::<Uid>(entity);
let maybe_player = state.ecs().write_storage::<comp::Player>().remove(entity);
let maybe_group = state
.ecs()
.write_storage::<group::Group>()
.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::<UidAllocator>()
.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::<group::GroupManager>();
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::<group::Group>().remove(entity);
// Delete old entity
if let Err(e) = state.delete_entity_recorded(entity) {
error!(

View File

@ -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::<Outcome>::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::<sys::TerrainTimer>().nanos as i64;
let waypoint_nanos = self.state.ecs().read_resource::<sys::WaypointTimer>().nanos as i64;
let invite_timeout_nanos = self
.state
.ecs()
.read_resource::<sys::InviteTimeoutTimer>()
.nanos as i64;
let stats_persistence_nanos = self
.state
.ecs()
.read_resource::<sys::PersistenceTimer>()
.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(),
});

View File

@ -26,6 +26,7 @@ pub struct ServerSettings {
pub persistence_db_dir: String,
pub max_view_distance: Option<u32>,
pub banned_words_files: Vec<PathBuf>,
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,
}
}
}

View File

@ -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::<Uid>(entity)
.read_component_copied::<Uid>(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::<comp::group::GroupManager>();
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::<Client>(), &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::<Client>(), &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::<Client>(), &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::<Client>(),
&ecs.read_storage::<comp::Group>(),
)
.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::<Client>();
let uids = self.ecs().read_storage::<Uid>();
let mut group_manager = self.ecs().write_resource::<comp::group::GroupManager>();
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::<Uid>().get(entity).copied(),
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),

View File

@ -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<Pos>>,
WriteStorage<'a, Last<Vel>>,
WriteStorage<'a, Last<Ori>>,
@ -40,6 +45,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, ForceUpdate>,
WriteStorage<'a, InventoryUpdate>,
Write<'a, DeletedEntities>,
Write<'a, Vec<Outcome>>,
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<f32>| {
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::<Vec<_>>();
if outcomes.len() > 0 {
client.notify(ServerMsg::Outcomes(outcomes));
}
}
outcomes.clear();
// Remove all force flags.
force_updates.clear();
inventory_updates.clear();

View File

@ -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<Self>>,
);
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::<Vec<_>>();
for entity in timed_out_invites {
invites.remove(entity);
}
timer.end();
}
}

View File

@ -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<specs::Entity>, ChatMsg)>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, UnresolvedChatMsg)>,
player_list: &HashMap<Uid, PlayerInfo>,
new_players: &mut Vec<specs::Entity>,
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<specs::Entity>, 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())

View File

@ -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<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type InviteTimeoutTimer = SysTimer<invite_timeout::Sys>;
pub type PersistenceTimer = SysTimer<persistence::Sys>;
pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
@ -32,12 +34,14 @@ pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
//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, &[]);
}

View File

@ -39,6 +39,7 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
power: 4.0,
owner: *owner,
friendly_damage: true,
});
}
},

View File

@ -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<Scale>>,
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
pub alignment: ReadExpect<'a, UpdateTracker<Alignment>>,
pub group: ReadExpect<'a, UpdateTracker<Group>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
@ -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<Scale>>,
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
group: WriteExpect<'a, UpdateTracker<Group>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>,
collider: WriteExpect<'a, UpdateTracker<Collider>>,
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
@ -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::<Scale>();
world.register_tracker::<Mounting>();
world.register_tracker::<MountState>();
world.register_tracker::<Alignment>();
world.register_tracker::<Group>();
world.register_tracker::<Mass>();
world.register_tracker::<Collider>();
world.register_tracker::<Sticky>();

View File

@ -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::<f32>::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::<u8>() < 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(),
),

View File

@ -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"

View File

@ -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"]

View File

@ -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);

View File

@ -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,

View File

@ -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::<f32>::from(self.control) * upper_torso;
let upper_torso_mat = torso_mat * upper_torso;
let lower_torso_mat = upper_torso_mat * Mat4::<f32>::from(self.lower_torso);
let head_mat = upper_torso_mat * Mat4::<f32>::from(self.head);
*(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [
make_bone(upper_torso_mat * Mat4::<f32>::from(self.head)),
make_bone(head_mat),
make_bone(head_mat * Mat4::<f32>::from(self.jaw)),
make_bone(upper_torso_mat),
make_bone(lower_torso_mat),
make_bone(lower_torso_mat * Mat4::<f32>::from(self.tail)),
make_bone(control_mat * Mat4::<f32>::from(self.main)),
make_bone(control_mat * Mat4::<f32>::from(self.second)),
make_bone(upper_torso_mat * Mat4::<f32>::from(self.shoulder_l)),
make_bone(upper_torso_mat * Mat4::<f32>::from(self.shoulder_r)),
make_bone(control_mat * Mat4::<f32>::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) {

View File

@ -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();

Some files were not shown because too many files have changed in this diff Show More