mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge remote-tracking branch 'origin/master' into sharp/small-fixes
This commit is contained in:
commit
0ed801d540
@ -5,3 +5,6 @@ rustflags = [
|
||||
|
||||
[alias]
|
||||
generate = "run --package tools --"
|
||||
test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features"
|
||||
server = "run --bin veloren-server-cli"
|
||||
|
||||
|
@ -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
5
Cargo.lock
generated
@ -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",
|
||||
|
18
README.md
18
README.md
@ -40,22 +40,6 @@ Due to rapid developement stable versions become outdated fast and might be **in
|
||||
|
||||
If you want to compile Veloren yourself, follow the instructions in our [Book](https://book.veloren.net/contributors/introduction.html).
|
||||
|
||||
### Packaging status
|
||||
|
||||
#### Fedora
|
||||
|
||||
[COPR repo](https://copr.fedorainfracloud.org/coprs/atim/veloren/): `sudo dnf copr enable atim/veloren -y && sudo dnf install veloren -y`
|
||||
|
||||
#### Arch
|
||||
|
||||
[AUR Airshipper](https://aur.archlinux.org/packages/airshipper-git): `yay -Syu airshipper-git`
|
||||
|
||||
[AUR latest binary release](https://aur.archlinux.org/packages/veloren-bin/): `yay -Syu veloren-bin`
|
||||
|
||||
[AUR latest release](https://aur.archlinux.org/packages/veloren/): `yay -Syu veloren`
|
||||
|
||||
[AUR latest master](https://aur.archlinux.org/packages/veloren-git): `yay -Syu veloren-git`
|
||||
|
||||
## F.A.Q.
|
||||
|
||||
### **Q:** How is this game licensed?
|
||||
@ -68,7 +52,7 @@ If you want to compile Veloren yourself, follow the instructions in our [Book](h
|
||||
|
||||
### **Q:** Do you accept donations?
|
||||
|
||||
**A:** To keep Veloren a passion project free from financial incentives we will **only accept donations to cover server hosting expenses.** There is no way to donate yet.
|
||||
**A:** You can support the project on our [OpenCollective Page](https://opencollective.com/veloren).
|
||||
|
||||
## Credit
|
||||
|
||||
|
@ -5,7 +5,7 @@ Item(
|
||||
(
|
||||
kind: Back("Short0"),
|
||||
stats: (
|
||||
protection: Normal(0.0),
|
||||
protection: Normal(0.2),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
12
assets/common/items/armor/back/short_1.ron
Normal file
12
assets/common/items/armor/back/short_1.ron
Normal file
@ -0,0 +1,12 @@
|
||||
Item(
|
||||
name: "Green Blanket",
|
||||
description: "Keeps your shoulders warm.",
|
||||
kind: Armor(
|
||||
(
|
||||
kind: Back("Short1"),
|
||||
stats: (
|
||||
protection: Normal(0.1),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
12
assets/common/items/armor/neck/neck_1.ron
Normal file
12
assets/common/items/armor/neck/neck_1.ron
Normal 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),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
@ -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"),
|
||||
|
@ -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"),
|
||||
]
|
||||
|
@ -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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
BIN
assets/voxygen/element/misc_bg/social_tab_online.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -388,5 +388,9 @@ Siła woli
|
||||
"esc_menu.quit_game": "Opuść gre",
|
||||
/// End Escape Menu Section
|
||||
/// Koniec sekcji Menu pauzy
|
||||
}
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
|
||||
},
|
||||
)
|
||||
|
@ -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",
|
||||
@ -324,6 +331,23 @@ magischen Gegenstände ergattern?"#,
|
||||
|
||||
"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",
|
||||
"hud.crafting.ingredients": "Zutaten:",
|
||||
@ -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
|
||||
|
||||
|
@ -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,6 +342,19 @@ magically infused items?"#,
|
||||
"hud.crafting.craft": "Craft",
|
||||
"hud.crafting.tool_cata": "Requires:",
|
||||
|
||||
"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",
|
||||
@ -389,6 +413,9 @@ 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!",
|
||||
@ -460,6 +488,8 @@ Protection
|
||||
"Press 'L-Shift' to open your Glider and conquer the skies.",
|
||||
"Veloren is still in Pre-Alpha. We do our best to improve it every day!",
|
||||
"If you want to join the Dev-Team or just have a chat with us join our Discord-Server.",
|
||||
"You can toggle showing your amount of health on the healthbar in the settings.",
|
||||
"In order to see your stats click the 'Stats' button in your inventory.",
|
||||
],
|
||||
"npc.speech.villager_under_attack": [
|
||||
"Help, I'm under attack!",
|
||||
|
@ -44,10 +44,10 @@ VoxygenLocalization(
|
||||
"common.resume": "Reprendre",
|
||||
"common.characters": "Personages",
|
||||
"common.close": "Fermer",
|
||||
"common.create": "Créer",
|
||||
"common.back": "Retour",
|
||||
"common.yes": "Oui",
|
||||
"common.no": "Non",
|
||||
"common.back": "Retour",
|
||||
"common.create": "Créer",
|
||||
"common.okay": "Compris",
|
||||
"common.accept": "Accepter",
|
||||
"common.disclaimer": "Avertissement",
|
||||
@ -56,6 +56,12 @@ VoxygenLocalization(
|
||||
"common.error": "Erreur",
|
||||
"common.fatal_error": "Erreur Fatale",
|
||||
|
||||
// Message when connection to the server is lost
|
||||
"common.connection_lost": r#"Connexion perdue !
|
||||
Le serveur a-il redémarré?
|
||||
Le client est-il à jour?"#,
|
||||
|
||||
|
||||
"common.species.orc": "Orc",
|
||||
"common.species.human": "Humain",
|
||||
"common.species.dwarf": "Nain",
|
||||
@ -69,27 +75,28 @@ VoxygenLocalization(
|
||||
"common.weapons.bow": "Arc",
|
||||
"common.weapons.hammer": "Marteau",
|
||||
|
||||
// Message when connection to the server is lost
|
||||
"common.connection_lost": r#"Connexion perdue!
|
||||
Le serveur a-il redémarré?
|
||||
Le client est-il à jour?"#,
|
||||
|
||||
|
||||
// Main screen texts
|
||||
"main.connecting": "Connexion",
|
||||
"main.creating_world": "Création du monde",
|
||||
"main.tip": "Astuce:",
|
||||
|
||||
// Annonce de bienvenui qui apparaît la première fois que Veloren est lancé
|
||||
"main.notice": r#"Bienvenue dans la version alpha de Veloren!
|
||||
|
||||
Avant de commencer à vous amuser, merci de garder les choses suivantes en tête:
|
||||
|
||||
- Il s'agit d'une version alpha très jeune. Attendez-vous à des bugs, un gameplay non terminé, des mécaniques non peaufinées et des fonctionalités manquantes.
|
||||
- Si vous avez des retours constructifs ou avez detecté un bug, vous pouvez nous contacter via Reddit, GitLab ou notre communauté Discord.
|
||||
- Veloren est un logiciel open-source sour licence GPL3. Cela signifit que vous êtes libre de jouer, modfier et redistribuer le jeu comme il vous semble (licence contaminante sous GPL 3 pour toute modification)
|
||||
- Veloren est un projet communautaire à but non-lucratif développé par des bénévolles.
|
||||
Si vous apprecier ce jeu, vous êtes les bienvenues pour rejoindre les équipes de développement ou d'artistes!
|
||||
- Le genre 'Voxel RPG' est un genre à part entiere. Les FPS étaient appelé Doom-like. De la même façon, nous essayons de construire un genre à part entière. Ce jeu n'est pas un clone et ses mechaniques changeront au cours du developpement.
|
||||
|
||||
Merci d'avoir pris le temps de lire cette notice, nous esperons que vous apprecierez le jeu!
|
||||
- Si vous avez des retours constructifs ou avez detecté un bug, vous pouvez nous contacter via Reddit, GitLab ou le serveur de notre communauté Discord.
|
||||
|
||||
- Veloren est un logiciel open-source sour licence GPL3. Cela signifit que vous êtes libre de jouer, modfier et redistribuer le jeu comme il vous semble (licence contaminante sous GPL 3 pour toute modification)
|
||||
|
||||
- Veloren est un projet communautaire à but non-lucratif développé par des bénévoles.
|
||||
Si vous apprecier ce jeu, vous êtes les bienvenues pour rejoindre les équipes de développement ou d'artistes!
|
||||
|
||||
Merci d'avoir pris le temps de lire cette annonce, nous espérons que vous apprecierez le jeu!
|
||||
|
||||
~ L'équipe de Veloren"#,
|
||||
|
||||
@ -107,7 +114,7 @@ https://account.veloren.net."#,
|
||||
"main.login.server_full": "Serveur plein",
|
||||
"main.login.untrusted_auth_server": "Le serveur d'authentification n'est pas de confiance",
|
||||
"main.login.outdated_client_or_server": "ServeurPasContent: Les versions sont probablement incompatibles, verifiez les mises à jour.",
|
||||
"main.login.timeout": "DélaiEcoulé: Le serveur n'a pas repondu à temps. (Surchage ou Problèmes réseau).",
|
||||
"main.login.timeout": "DélaiEcoulé: Le serveur n'a pas repondu à temps. (Surchage ou problèmes réseau).",
|
||||
"main.login.server_shut_down": "Extinction du Serveur",
|
||||
"main.login.already_logged_in": "Vous êtes déjà connecté à ce serveur.",
|
||||
"main.login.network_error": "Problème Réseau",
|
||||
@ -115,10 +122,10 @@ https://account.veloren.net."#,
|
||||
"main.login.invalid_character": "Le personnage sélectionné n'est pas valide",
|
||||
"main.login.client_crashed": "Le client a planté",
|
||||
"main.login.not_on_whitelist": "Vous devez être ajouté à la liste blanche par un Admin pour pouvoir entrer",
|
||||
"main.tip": "Astuce:",
|
||||
|
||||
/// End Main screen section
|
||||
|
||||
|
||||
///Début section Hud
|
||||
"hud.do_not_show_on_startup": "Ne pas afficher au démarage",
|
||||
"hud.show_tips": "Voir les astuces",
|
||||
@ -126,11 +133,10 @@ https://account.veloren.net."#,
|
||||
"hud.you_died": "Vous êtes mort",
|
||||
"hud.waypoint_saved": "Point de Repère Sauvegardé",
|
||||
|
||||
|
||||
"hud.press_key_to_show_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles",
|
||||
"hud.press_key_to_show_debug_info_fmt": "Appuyer sur {key} pour afficher les informations de debogage",
|
||||
"hud.press_key_to_toggle_debug_info_fmt": "Appuyer sur {key} pour activer les informations de debogage",
|
||||
"hud.press_key_to_toggle_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles",
|
||||
"hud.press_key_to_toggle_debug_info_fmt": "Appuyer sur {key} pour activer les informations de debogage",
|
||||
|
||||
// Sorties Tchat
|
||||
"hud.chat.online_msg": "[{name}] est maintenant en ligne.",
|
||||
@ -147,7 +153,6 @@ https://account.veloren.net."#,
|
||||
// Respawn message
|
||||
"hud.press_key_to_respawn": r#"Appuyez sur {key} pour réapparaitre au dernier feu de camp visité"#,
|
||||
|
||||
|
||||
/// Welcome message
|
||||
"hud.welcome": r#"Bienvenue dans la version Alpha de Veloren!
|
||||
|
||||
@ -155,16 +160,12 @@ https://account.veloren.net."#,
|
||||
Quelques astuces avant de démarrer:
|
||||
|
||||
|
||||
POINT LE PLUS IMPORTANT: Pour configurer votre point de resurection tapez
|
||||
/waypoint dans le chat. (y-compris si vous être déjà mort!)
|
||||
|
||||
|
||||
Appuyer sur F1 pour voir les commandes disponibles.
|
||||
|
||||
Tapez /help dans le chat pour voir les commandes du chat.
|
||||
|
||||
|
||||
Des coffres et autres objets sont disposés aléatoirement dans le monde!
|
||||
Des coffres et autres objets sont disposés aléatoirement dans le monde !
|
||||
|
||||
Utilisez le click droit pour le collecter.
|
||||
|
||||
@ -174,15 +175,18 @@ Double cliquez sur les éléments de votre sac pour les utiliser ou les équiper
|
||||
|
||||
Jettez-les en cliquant sur un element puis en cliquant en dehors du sac.
|
||||
|
||||
|
||||
Les nuits peuvent être très sombre à Veloren.
|
||||
|
||||
Allumez votre lanterne en tapant /lantern dans le chat.
|
||||
Allumez votre lanterne en appuyant sur 'G'.
|
||||
|
||||
|
||||
Vous souhaitez libérer votre souris pour fermer cette fenêtre? Appuyez sur TAB!
|
||||
|
||||
Vous souhaitez libérer votre souris pour fermer cette fenêtre? Tapez sur TAB!
|
||||
|
||||
Profitez de votre séjour dans le monde de Veloren."#,
|
||||
|
||||
"hud.temp_quest_headline": r#"S'il vous plaît, aidez nous voyageur!"#,
|
||||
"hud.temp_quest_headline": r#"S'il vous plaît, aidez-nous voyageur!"#,
|
||||
"hud.temp_quest_text": r#"Des donjons remplis de cultistes malfaisants
|
||||
sont apparus tout autour de nos paisibles villages!
|
||||
|
||||
@ -204,7 +208,7 @@ objets magiques ?"#,
|
||||
"hud.bag.stats": "Attributs",
|
||||
"hud.bag.head": "Tête",
|
||||
"hud.bag.neck": "Cou",
|
||||
"hud.bag.tabard": "Tabar",
|
||||
"hud.bag.tabard": "Tabard",
|
||||
"hud.bag.shoulders": "Epaules",
|
||||
"hud.bag.chest": "Torse",
|
||||
"hud.bag.hands": "Mains",
|
||||
@ -215,17 +219,17 @@ objets magiques ?"#,
|
||||
"hud.bag.legs": "Jambes",
|
||||
"hud.bag.feet": "Pieds",
|
||||
"hud.bag.mainhand": "Main Dominante",
|
||||
"hud.bag.offhand": "Main Dominée",
|
||||
"hud.bag.offhand": "Main Secondaire",
|
||||
|
||||
|
||||
// Carte et journal de quetes
|
||||
"hud.map.map_title": "Carte",
|
||||
"hud.map.qlog_title": "Quêtes",
|
||||
|
||||
|
||||
//Paramètres
|
||||
"hud.settings.general": "Général",
|
||||
"hud.settings.none": "Aucun",
|
||||
"hud.settings.press_behavior.toggle": "Activer/Desactiver",
|
||||
"hud.settings.press_behavior.toggle": "Activer/Désactiver",
|
||||
"hud.settings.press_behavior.hold": "Maintenir",
|
||||
"hud.settings.help_window": "Fenêtre d'aide",
|
||||
"hud.settings.debug_info": "Information de débogage",
|
||||
@ -237,12 +241,12 @@ objets magiques ?"#,
|
||||
"hud.settings.transparency": "Transparence",
|
||||
"hud.settings.hotbar": "Barre d'action",
|
||||
"hud.settings.toggle_shortcuts": "Activer les raccourcis",
|
||||
"hud.settings.toggle_bar_experience": "Activer la barre d'experience",
|
||||
"hud.settings.toggle_bar_experience": "Activer la barre d'expérience",
|
||||
"hud.settings.scrolling_combat_text": "Dégats de combat",
|
||||
"hud.settings.single_damage_number": "Dégat adversaire (par dégat)",
|
||||
"hud.settings.cumulated_damage": "Dégat adversaire (cumulé)",
|
||||
"hud.settings.incoming_damage": "Dégat personnage (par dégat)",
|
||||
"hud.settings.cumulated_incoming_damage": "Dégat personnage (cumulé)",
|
||||
"hud.settings.single_damage_number": "Dégats infligés",
|
||||
"hud.settings.cumulated_damage": "Dégat infligés cumulés",
|
||||
"hud.settings.incoming_damage": "Dégats reçus",
|
||||
"hud.settings.cumulated_incoming_damage": "Dégats reçus cumulés",
|
||||
"hud.settings.speech_bubble": "Bulle de dialogue",
|
||||
"hud.settings.speech_bubble_dark_mode": "Bulle de dialogue Mode Sombre",
|
||||
"hud.settings.speech_bubble_icon": "Icône Bulle de dialogue",
|
||||
@ -251,8 +255,8 @@ objets magiques ?"#,
|
||||
"hud.settings.percentages": "Pourcentages",
|
||||
"hud.settings.chat": "Tchat",
|
||||
"hud.settings.background_transparency": "Transparence du fond",
|
||||
"hud.settings.chat_character_name": "Nom des personnages dans le tchat",
|
||||
"hud.settings.loading_tips": "Astuces de chargement",
|
||||
"hud.settings.chat_character_name": "Noms des personnages dans le tchat",
|
||||
"hud.settings.loading_tips": "Astuces sur l'écran de chargement",
|
||||
|
||||
"hud.settings.pan_sensitivity": "Sensibilité de la souris",
|
||||
"hud.settings.zoom_sensitivity": "Sensibilité du zoom",
|
||||
@ -287,11 +291,11 @@ objets magiques ?"#,
|
||||
"hud.settings.reset_keybinds": "Réinitialiser touches par défaut",
|
||||
|
||||
"hud.social": "Social",
|
||||
"hud.social.online": "En ligne",
|
||||
"hud.social.friends": "Amis",
|
||||
"hud.social.faction": "Faction",
|
||||
"hud.social.online": "Jeu en ligne",
|
||||
"hud.social.not_yet_available": "Pas encore disponible",
|
||||
"hud.social.play_online_fmt": "{nb_player} joueurs en ligne",
|
||||
"hud.social.faction": "Faction",
|
||||
"hud.social.play_online_fmt": "{nb_player} joueur(s) en ligne",
|
||||
|
||||
"hud.crafting": "Fabrication",
|
||||
"hud.crafting.recipes": "Recettes",
|
||||
@ -304,9 +308,11 @@ objets magiques ?"#,
|
||||
"hud.free_look_indicator": "Vue libre active",
|
||||
"hud.auto_walk_indicator": "Marche automatique active",
|
||||
|
||||
//Fin Section Hud
|
||||
/// Fin Section Hud
|
||||
|
||||
|
||||
/// Debut de section GameInput
|
||||
|
||||
"gameinput.primary": "Attaque Basique",
|
||||
"gameinput.secondary": "Attaque Secondaire/Bloquer/Viser",
|
||||
"gameinput.slot1": "Emplacement rapide 1",
|
||||
@ -323,7 +329,7 @@ objets magiques ?"#,
|
||||
"gameinput.togglecursor": "Activer/Desactiver Curseur",
|
||||
"gameinput.help": "Activer/Desactiver Fenêtre d'aide",
|
||||
"gameinput.toggleinterface": "Activer/Desactiver Interface",
|
||||
"gameinput.toggledebug": "Activer/Desactiver IPS et Infos Debogage",
|
||||
"gameinput.toggledebug": "Activer/Desactiver FPS et Infos Debogage",
|
||||
"gameinput.screenshot": "Prendre une capture d'écran",
|
||||
"gameinput.toggleingameui": "Activer/Desactiver Noms de joueurs",
|
||||
"gameinput.fullscreen": "Activer/Desactiver Plein Ecran",
|
||||
@ -341,7 +347,7 @@ objets magiques ?"#,
|
||||
"gameinput.mount": "Monture",
|
||||
"gameinput.enter": "Entrer",
|
||||
"gameinput.command": "Commande",
|
||||
"gameinput.escape": "Fuir",
|
||||
"gameinput.escape": "Menu principal/Fermer menu",
|
||||
"gameinput.map": "Carte",
|
||||
"gameinput.bag": "Sac",
|
||||
"gameinput.social": "Social",
|
||||
@ -358,43 +364,41 @@ objets magiques ?"#,
|
||||
|
||||
/// End GameInput section
|
||||
|
||||
/// Debut Section Menu Start Quitter
|
||||
"esc_menu.quit_game": "Quitter le jeu",
|
||||
"esc_menu.logout": "Se déconnecter",
|
||||
|
||||
/// Fin Section Menu Start Quitter
|
||||
|
||||
/// Debut de la section Création du personnage
|
||||
"char_selection.accessories": "Accessoires",
|
||||
"char_selection.beard": "Barbe",
|
||||
"char_selection.loading_characters": "Chargement des personnages...",
|
||||
"char_selection.delete_permanently": "Supprimer définitivement ce personnage ?",
|
||||
"char_selection.deleting_character": "Suppression du personnage...",
|
||||
"char_selection.change_server": "Changer de serveur",
|
||||
"char_selection.enter_world": "Entrer dans le monde",
|
||||
"char_selection.logout": "Se déconnecter",
|
||||
"char_selection.create_new_charater": "Créer un nouveau personnage",
|
||||
"char_selection.creating_character": "Création du personnage...",
|
||||
"char_selection.character_creation": "Création de personnages",
|
||||
"char_selection.create_info_name": "Votre personnage doit avoir un prénom !",
|
||||
"char_selection.deleting_character": "Suppression du personnage...",
|
||||
"char_selection.enter_world": "Entrer dans le monde",
|
||||
"char_selection.eyebrows": "Sourcils",
|
||||
"char_selection.eye_color": "Couleur des yeux",
|
||||
"char_selection.eyeshape": "Forme des yeux",
|
||||
"char_selection.hair_style": "Coupe de cheveux",
|
||||
"char_selection.hair_color": "Couleur des cheveux",
|
||||
"char_selection.character_creation": "Création de personnage",
|
||||
|
||||
"char_selection.human_default": "Humain par défault",
|
||||
"char_selection.level_fmt": "Niveau {level_nb}",
|
||||
"char_selection.logout": "Se déconnecter",
|
||||
"char_selection.loading_characters": "Chargement des personnages...",
|
||||
"char_selection.plains_of_uncertainty": "Plaines de l'incertitude",
|
||||
"char_selection.skin": "Couleur de peau",
|
||||
"char_selection.uncanny_valley": "Vallée dérangeante",
|
||||
"char_selection.change_server": "Changer de serveur",
|
||||
"char_selection.delete_permanently": "Supprimer définitivement ce personnage ?",
|
||||
"char_selection.uncanny_valley": "Région sauvage",
|
||||
"char_selection.plains_of_uncertainty": "Plaines de l'Incertitude",
|
||||
"char_selection.beard": "Barbe",
|
||||
"char_selection.hair_style": "Coupe de cheveux",
|
||||
"char_selection.hair_color": "Couleur des cheveux",
|
||||
"char_selection.eye_color": "Couleur des yeux",
|
||||
"char_selection.skin": "Couleur de la peau",
|
||||
"char_selection.eyeshape": "Forme des yeux",
|
||||
"char_selection.accessories": "Accessoires",
|
||||
"char_selection.create_info_name": "Votre personnage doit avoir un prénom !",
|
||||
|
||||
/// Fin de la section Création du personnage
|
||||
|
||||
/// Start character window section
|
||||
"character_window.character_name": "Personnage",
|
||||
// Character stats
|
||||
"character_window.character_stats": r#"Endurance
|
||||
|
||||
Force
|
||||
|
||||
Dexterité
|
||||
Volonté
|
||||
|
||||
Protection
|
||||
"#,
|
||||
@ -408,105 +412,106 @@ Protection
|
||||
|
||||
},
|
||||
|
||||
|
||||
vector_map: {
|
||||
"loading.tips": [
|
||||
"Appuie sur 'G' pour allumer ta lanterne.",
|
||||
"Appuie sur 'F1' pour voir les raccourcis clavier par défaut.",
|
||||
"Tu peux écrire /say ou /s pour discuter aux joueurs directement à côté toi.",
|
||||
"Tu peux écrire /region ou /r pour discuter avec les joueurs situés à quelques centaines de blocs de toi.",
|
||||
"Pour envoyer un message privé, écrit /tell suivi par un nom de joueur puis ton message.",
|
||||
"Appuiez sur 'G' pour allumer ta lanterne.",
|
||||
"Appuiez sur 'F1' pour voir les raccourcis clavier par défaut.",
|
||||
"Vous pouvez taper /say ou /s pour discuter aux joueurs directement à côté toi.",
|
||||
"Vous pouvez taper /region ou /r pour discuter avec les joueurs situés à quelques centaines de blocs de toi.",
|
||||
"Pour envoyer un message privé, tapez /tell suivi par un nom de joueur puis votre message.",
|
||||
"Des PNJs avec le même niveau peuvent varier en difficulté.",
|
||||
"Regarde le sol pour trouver de la nourriture, des coffres et d'autres butins!",
|
||||
"Ton inventaire est rempli de nourriture? Essaie de créer un meilleur repas avec!",
|
||||
"Tu cherches une activité? Les donjons sont marqués avec des points marron sur la carte!",
|
||||
"N'oublie pas d'ajuster les graphiques pour ton système. Appuie sur 'N' pour ouvrir les paramètres.",
|
||||
"Jouer à plusieurs est amusant! Appuie sur 'O' pour voir qui est en ligne.",
|
||||
"Un PNJ avec une tête-de-mort sous sa barre de vie est plus puissant que toi.",
|
||||
"Appuie sur 'J' pour danser. C'est la fête!",
|
||||
"Appuie sur 'L-Shift'pour ouvrir ton deltaplane et conquérir les cieux.",
|
||||
"Regardez le sol pour trouver de la nourriture, des coffres et d'autres butins!",
|
||||
"Votre inventaire est rempli de nourriture? Essayez de créer un meilleur repas avec!",
|
||||
"Vous cherchez une activité? Les donjons sont marqués avec des points marron sur la carte !",
|
||||
"N'oubliez pas d'ajuster les graphiques pour votre système. Appuyez sur 'N' pour ouvrir les paramètres.",
|
||||
"Jouer à plusieurs est amusant! Appuyez sur 'O' pour voir qui est en ligne.",
|
||||
"Un PNJ avec une tête de mort sous sa barre de vie est plus puissant que vous.",
|
||||
"Appuyez sur 'J' pour danser. C'est la fête !",
|
||||
"Appuyez sur 'L-Shift'pour ouvrir votre deltaplane et conquérir les cieux.",
|
||||
"Veloren est encore Pre-Alpha. Nous faisons de notre mieux pour l'améliorer chaque jour!",
|
||||
"Si tu veux te joindre à l'équipe de développement ou juste discuter avec nous, rejoins notre serveur Discord.",
|
||||
"Si vous voulez vous joindre à l'équipe de développement ou juste discuter avec nous, rejoignez notre serveur Discord.",
|
||||
],
|
||||
"npc.speech.villager_under_attack": [
|
||||
"À l'aide, on m'attaque!",
|
||||
"À l'aide! On m'attaque!",
|
||||
"Aïe! On m'attaque!",
|
||||
"Aïe! On m'attaque! À l'aide!",
|
||||
"Aidez-moi! On m'attaque!",
|
||||
"On m'attaque! À l'aide!",
|
||||
"On m'attaque! Aidez-moi!",
|
||||
"À l'aide!",
|
||||
"À l'aide! À l'aide!",
|
||||
"À l'aide! À l'aide! À l'aide!",
|
||||
"On m'attaque!",
|
||||
"AAAHHH! On m'attaque!",
|
||||
"AAAHHH! On m'attaque! À l'aide!",
|
||||
"À l'aide! Nous sommes attaqués!",
|
||||
"À l'aide! Assassin!",
|
||||
"À l'aide! Il y a un assassin en liberté!",
|
||||
"À l'aide! On essaie de me tuer!",
|
||||
"Gardes, on m'attaque!",
|
||||
"Gardes! On m'attaque!",
|
||||
"On m'attaque! Gardes!",
|
||||
"À l'aide! Gardes! On m'attaque!",
|
||||
"Gardes! Venez vite!",
|
||||
"Gardes! Gardes!",
|
||||
"Gardes! Un scélérat m'attaque!",
|
||||
"Gardes, abattez ce scélérat!",
|
||||
"Gardes! Il y a un meurtrier!",
|
||||
"Gardes! Aidez-moi!",
|
||||
"Vous ne vous en tirerez pas comme ça! Gardes!",
|
||||
"Monstre!",
|
||||
"À l'aide, on m'attaque !",
|
||||
"À l'aide ! On m'attaque !",
|
||||
"Aïe ! On m'attaque !",
|
||||
"Aïe ! On m'attaque ! À l'aide !",
|
||||
"Aidez-moi! On m'attaque !",
|
||||
"On m'attaque ! À l'aide !",
|
||||
"On m'attaque ! Aidez-moi !",
|
||||
"À l'aide !",
|
||||
"À l'aide ! À l'aide !",
|
||||
"À l'aide ! À l'aide ! À l'aide !",
|
||||
"On m'attaque !",
|
||||
"AAAHHH ! On m'attaque !",
|
||||
"AAAHHH ! On m'attaque ! À l'aide !",
|
||||
"À l'aide ! Nous sommes attaqués !",
|
||||
"À l'aide ! Assassin !",
|
||||
"À l'aide ! Il y a un assassin en liberté !",
|
||||
"À l'aide ! On essaie de me tuer !",
|
||||
"Gardes, on m'attaque !",
|
||||
"Gardes ! On m'attaque !",
|
||||
"On m'attaque ! Gardes !",
|
||||
"À l'aide ! Gardes ! On m'attaque !",
|
||||
"Gardes ! Venez vite !",
|
||||
"Gardes ! Gardes !",
|
||||
"Gardes ! Un scélérat m'attaque !",
|
||||
"Gardes, abattez ce scélérat !",
|
||||
"Gardes ! Il y a un meurtrier !",
|
||||
"Gardes ! Aidez-moi!",
|
||||
"Vous ne vous en tirerez pas comme ça! Gardes !",
|
||||
"Monstre !",
|
||||
"Aidez-moi!",
|
||||
"À l'aide! S'il vous plait!",
|
||||
"Aïe! Gardes! À l'aide!",
|
||||
"À l'aide ! S'il vous plait !",
|
||||
"Aïe ! Gardes ! À l'aide !",
|
||||
"Ils viennent pour moi !",
|
||||
"À l'aide! À l'aide! Je me fais réprimer!",
|
||||
"À l'aide ! À l'aide ! Je me fais réprimer !",
|
||||
"Ah, nous voyons maintenant la violence inhérente au système.",
|
||||
"C'est seulement une égratignure.",
|
||||
"Arrêtez ça!",
|
||||
"Qu'est ce que je t'ai fait?!",
|
||||
"S'il te plaît arrête de m'attaquer!",
|
||||
"Hé! Regardez où vous pointez cette chose!",
|
||||
"Misérable, allez-vous-en!",
|
||||
"Arrêtez! Partez! Arrêtez!",
|
||||
"Vous m'avez ennervé!",
|
||||
"Oi! Qui croyez-vous être?!",
|
||||
"J'aurais votre tête pour ça!",
|
||||
"Stoppez s'il vous plaît! Je ne transporte rien de valeur!",
|
||||
"Je vais appeler mon frère, il est plus grand que moi!",
|
||||
"Nooon, Je vais le dire à ma mère!",
|
||||
"Soyez maudit!",
|
||||
"Arrêtez ça !",
|
||||
"Qu'est ce que je vous ai fait ?!",
|
||||
"S'il vous plaît arrêtez de m'attaquer !",
|
||||
"Hé! Regardez où vous pointez cette chose !",
|
||||
"Misérable, allez-vous-en !",
|
||||
"Arrêtez ! Partez ! Arrêtez !",
|
||||
"Vous m'avez ennervé !",
|
||||
"Oi ! Qui croyez-vous être ?!",
|
||||
"J'aurais votre tête pour ça !",
|
||||
"Arrêtez, s'il vous plaît ! Je ne transporte rien de valeur !",
|
||||
"Je vais appeler mon frère, il est plus grand que moi !",
|
||||
"Nooon, Je vais le dire à ma mère !",
|
||||
"Soyez maudit !",
|
||||
"Ne faites pas ça.",
|
||||
"Ce n'était pas très gentil!",
|
||||
"Ton arme fonctionne, tu peux la ranger maintenant!",
|
||||
"Épargnez-moi!",
|
||||
"Pitié, J'ai une famille!",
|
||||
"Je suis trop jeune pour mourrir!",
|
||||
"On peut en parler?",
|
||||
"La violence n'est jamais la solution!",
|
||||
"Ce n'était pas très gentil !",
|
||||
"Ton arme fonctionne, tu peux la ranger maintenant !",
|
||||
"Épargnez-moi !",
|
||||
"Pitié, J'ai une famille !",
|
||||
"Je suis trop jeune pour mourrir !",
|
||||
"On peut en parler ?",
|
||||
"La violence n'est jamais la solution !",
|
||||
"Aujourd'hui est une très mauvaise journée...",
|
||||
"Hé, ça fait mal!",
|
||||
"Aïe!",
|
||||
"Quelle impolitesse!",
|
||||
"Stop, je vous en prie!",
|
||||
"Que la peste vous emporte!",
|
||||
"Hé, ça fait mal !",
|
||||
"Aïe !",
|
||||
"Quelle impolitesse !",
|
||||
"Stop, je vous en prie !",
|
||||
"Que la peste vous emporte !",
|
||||
"Ce n'est pas amusant.",
|
||||
"Comment osez-vous?!",
|
||||
"Vous allez payer!",
|
||||
"Continue et tu vas le regretter!",
|
||||
"Ne m'obligez pas à vous faire du mal!",
|
||||
"Il doit y avoir erreur!",
|
||||
"Vous n'avez pas besoin de faire ça!",
|
||||
"Fuyez, monstre!",
|
||||
"Ça fait vraiment mal!",
|
||||
"Pourquoi faites-vous cela?",
|
||||
"Par les esprits, cessez!",
|
||||
"Vous devez m'avoir confondu avec quelqu'un d'autre!",
|
||||
"Je ne mérite pas cela!",
|
||||
"Comment osez-vous ?!",
|
||||
"Vous allez payer !",
|
||||
"Continue et tu vas le regretter !",
|
||||
"Ne m'obligez pas à vous faire du mal !",
|
||||
"Il doit y avoir erreur !",
|
||||
"Vous n'avez pas besoin de faire ça !",
|
||||
"Fuyez, monstre !",
|
||||
"Ça fait vraiment mal !",
|
||||
"Pourquoi faites-vous cela ?",
|
||||
"Par les esprits, cessez !",
|
||||
"Vous devez m'avoir confondu avec quelqu'un d'autre !",
|
||||
"Je ne mérite pas cela !",
|
||||
"Ne faites plus cela.",
|
||||
"Gardes, jetez ce monstre dans le lac!",
|
||||
"Je vais t'envoyer ma tarrasque!",
|
||||
"Gardes, jetez ce monstre dans le lac !",
|
||||
"Je vais t'envoyer ma tarrasque !",
|
||||
],
|
||||
}
|
||||
)
|
@ -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: {
|
||||
|
||||
},
|
||||
)
|
||||
|
@ -377,5 +377,9 @@ Veloren 半夜會特別暗。
|
||||
"esc_menu.logout": "登出",
|
||||
"esc_menu.quit_game": "退出遊戲",
|
||||
/// End Escape Menu Section
|
||||
}
|
||||
},
|
||||
|
||||
vector_map: {
|
||||
|
||||
},
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
86
assets/voxygen/shaders/particle-frag.glsl
Normal file
86
assets/voxygen/shaders/particle-frag.glsl
Normal 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));
|
||||
}
|
140
assets/voxygen/shaders/particle-vert.glsl
Normal file
140
assets/voxygen/shaders/particle-vert.glsl
Normal 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
BIN
assets/voxygen/voxel/armor/back/short-1.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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"),
|
||||
|
@ -16,5 +16,9 @@
|
||||
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
BIN
assets/voxygen/voxel/particle.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/weapon/staff/firestaff_cultist.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/world/structure/dungeon/misc_entrance/tower-ruin.vox
(Stored with Git LFS)
BIN
assets/world/structure/dungeon/misc_entrance/tower-ruin.vox
(Stored with Git LFS)
Binary file not shown.
@ -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,22 +160,32 @@ 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 {
|
||||
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 is running {}[{}], you are running {}[{}], versions might \
|
||||
be incompatible!",
|
||||
server_info.git_hash,
|
||||
server_info.git_date,
|
||||
common::util::GIT_HASH.to_string(),
|
||||
@ -184,9 +205,7 @@ impl Client {
|
||||
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: {:?}",
|
||||
@ -201,9 +220,7 @@ impl Client {
|
||||
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(),
|
||||
));
|
||||
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()));
|
||||
@ -265,10 +282,8 @@ impl Client {
|
||||
} else {
|
||||
Some(
|
||||
Vec2::new(
|
||||
(downhill as usize % map_size.x as usize)
|
||||
as i32,
|
||||
(downhill as usize / map_size.x as usize)
|
||||
as i32,
|
||||
(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),
|
||||
)
|
||||
};
|
||||
@ -339,6 +354,7 @@ impl Client {
|
||||
lod_horizon,
|
||||
(world_map, map_size, map_bounds),
|
||||
recipe_book,
|
||||
max_group_size,
|
||||
));
|
||||
},
|
||||
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
|
||||
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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)]
|
||||
|
@ -41,6 +41,7 @@ pub enum CharacterState {
|
||||
Climb,
|
||||
Sit,
|
||||
Dance,
|
||||
Sneak,
|
||||
Glide,
|
||||
GlideWield,
|
||||
/// A basic blocking state
|
||||
|
@ -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
|
||||
///
|
||||
|
@ -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
528
common/src/comp/group.rs
Normal 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 => {},
|
||||
});
|
||||
}
|
||||
}
|
@ -261,6 +261,7 @@ impl Tool {
|
||||
col: (0.85, 0.5, 0.11).into(),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
projectile_gravity: None,
|
||||
},
|
||||
BasicRanged {
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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
30
common/src/outcome.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
56
common/src/states/sneak.rs
Normal file
56
common/src/states/sneak.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
||||
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 {
|
||||
|
@ -77,6 +77,7 @@ impl<'a> System<'a> for Sys {
|
||||
CharacterState::Idle { .. }
|
||||
| CharacterState::Sit { .. }
|
||||
| CharacterState::Dance { .. }
|
||||
| CharacterState::Sneak { .. }
|
||||
| CharacterState::Glide { .. }
|
||||
| CharacterState::GlideWield { .. }
|
||||
| CharacterState::Wielding { .. }
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
(|| {
|
||||
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) } =
|
||||
let by = 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,
|
||||
);
|
||||
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;
|
||||
|
452
server/src/events/group_manip.rs
Normal file
452
server/src/events/group_manip.rs
Normal 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(),
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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!(
|
||||
|
@ -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(),
|
||||
});
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
71
server/src/sys/invite_timeout.rs
Normal file
71
server/src/sys/invite_timeout.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
@ -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, &[]);
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ impl<'a> System<'a> for Sys {
|
||||
pos: pos.0,
|
||||
power: 4.0,
|
||||
owner: *owner,
|
||||
friendly_damage: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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>();
|
||||
|
@ -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(),
|
||||
),
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -79,6 +79,11 @@ impl Animation for WieldAnimation {
|
||||
* Quaternion::rotation_z(1.0);
|
||||
next.main.scale = Vec3::one() * 1.02;
|
||||
|
||||
next.second.position = Vec3::new(0.0, 0.0, 0.0);
|
||||
next.second.orientation =
|
||||
Quaternion::rotation_x(PI) * Quaternion::rotation_y(0.0) * Quaternion::rotation_z(0.0);
|
||||
next.second.scale = Vec3::one() * 0.0;
|
||||
|
||||
next.hand_l.position = Vec3::new(
|
||||
-skeleton_attr.hand.0 - 7.0,
|
||||
skeleton_attr.hand.1 - 7.0,
|
||||
@ -123,6 +128,14 @@ impl Animation for WieldAnimation {
|
||||
Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0);
|
||||
next.lower_torso.scale = Vec3::one() * 1.02;
|
||||
|
||||
next.jaw.position = Vec3::new(0.0, skeleton_attr.jaw.0, skeleton_attr.jaw.1 * 0.0);
|
||||
next.jaw.orientation = Quaternion::rotation_z(0.0);
|
||||
next.jaw.scale = Vec3::one();
|
||||
|
||||
next.tail.position = Vec3::new(0.0, skeleton_attr.tail.0, skeleton_attr.tail.1);
|
||||
next.tail.orientation = Quaternion::rotation_z(0.0);
|
||||
next.tail.scale = Vec3::one();
|
||||
|
||||
next.shoulder_l.position = Vec3::new(
|
||||
-skeleton_attr.shoulder.0,
|
||||
skeleton_attr.shoulder.1,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user