mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'imbris/figraycast' into 'master'
Add detection of entities under the cursor in Voxygen and Group functions Closes #511 and #534 See merge request veloren/veloren!942
This commit is contained in:
commit
7aebff26e0
@ -4,4 +4,7 @@ rustflags = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
generate = "run --package tools --"
|
generate = "run --package tools --"
|
||||||
|
test-server = "run --bin veloren-server-cli --no-default-features"
|
||||||
|
server = "run --bin veloren-server-cli"
|
||||||
|
|
||||||
|
@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Loading-Screen tips
|
- Loading-Screen tips
|
||||||
- Feeding animation for some animals
|
- Feeding animation for some animals
|
||||||
- Power stat to weapons which affects weapon damage
|
- Power stat to weapons which affects weapon damage
|
||||||
|
- Add detection of entities under the cursor
|
||||||
|
- Functional group-system with exp-sharing and disabled damage to group members
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -80,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Fixed window resizing on Mac OS X.
|
- Fixed window resizing on Mac OS X.
|
||||||
- Dehardcoded many item variants
|
- Dehardcoded many item variants
|
||||||
- Tooltips avoid the mouse better and disappear when hovered
|
- Tooltips avoid the mouse better and disappear when hovered
|
||||||
|
- Improved social window functions and visuals
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4638,6 +4638,7 @@ dependencies = [
|
|||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"slab",
|
||||||
"specs",
|
"specs",
|
||||||
"specs-idvs",
|
"specs-idvs",
|
||||||
"sum_type",
|
"sum_type",
|
||||||
|
@ -5,7 +5,7 @@ Item(
|
|||||||
(
|
(
|
||||||
kind: Back("Short0"),
|
kind: Back("Short0"),
|
||||||
stats: (
|
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(
|
Item(
|
||||||
name: "Uneven Bow",
|
name: "Uneven Bow",
|
||||||
description: "Someone carved his initials into it.",
|
description: "Someone carved their initials into it.",
|
||||||
kind: Tool(
|
kind: Tool(
|
||||||
(
|
(
|
||||||
kind: Bow("ShortBow0"),
|
kind: Bow("ShortBow0"),
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
(0.50, "common.items.weapons.staff.starter_staff"),
|
(0.50, "common.items.weapons.staff.starter_staff"),
|
||||||
(0.35, "common.items.weapons.staff.bone_staff"),
|
(0.35, "common.items.weapons.staff.bone_staff"),
|
||||||
(0.15, "common.items.weapons.staff.amethyst_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
|
// hammers
|
||||||
(0.05, "common.items.weapons.hammer.starter_hammer"),
|
(0.05, "common.items.weapons.hammer.starter_hammer"),
|
||||||
(0.05, "common.items.weapons.hammer.wood_hammer-0"),
|
(0.05, "common.items.weapons.hammer.wood_hammer-0"),
|
||||||
@ -230,6 +230,8 @@
|
|||||||
(0.6, "common.items.armor.ring.ring_0"),
|
(0.6, "common.items.armor.ring.ring_0"),
|
||||||
// capes
|
// capes
|
||||||
(0.6, "common.items.armor.back.short_0"),
|
(0.6, "common.items.armor.back.short_0"),
|
||||||
|
(0.7, "common.items.armor.back.short_1"),
|
||||||
// necks
|
// necks
|
||||||
(0.6, "common.items.armor.neck.neck_0"),
|
(0.6, "common.items.armor.neck.neck_0"),
|
||||||
|
(0.4, "common.items.armor.neck.neck_1"),
|
||||||
]
|
]
|
||||||
|
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.
@ -68,6 +68,8 @@ VoxygenLocalization(
|
|||||||
"common.none": "Kein",
|
"common.none": "Kein",
|
||||||
"common.error": "Fehler",
|
"common.error": "Fehler",
|
||||||
"common.fatal_error": "Fataler Fehler",
|
"common.fatal_error": "Fataler Fehler",
|
||||||
|
"common.decline": "Ablehnen",
|
||||||
|
"common.you": "Ihr",
|
||||||
/// End Common section
|
/// End Common section
|
||||||
|
|
||||||
// Message when connection to the server is lost
|
// Message when connection to the server is lost
|
||||||
@ -306,7 +308,7 @@ magischen Gegenstände ergattern?"#,
|
|||||||
"hud.settings.unbound": "-",
|
"hud.settings.unbound": "-",
|
||||||
"hud.settings.reset_keybinds": "Auf Standard zurücksetzen",
|
"hud.settings.reset_keybinds": "Auf Standard zurücksetzen",
|
||||||
|
|
||||||
"hud.social": "Sozial",
|
"hud.social": "Andere Spieler",
|
||||||
"hud.social.online": "Online",
|
"hud.social.online": "Online",
|
||||||
"hud.social.friends": "Freunde",
|
"hud.social.friends": "Freunde",
|
||||||
"hud.social.not_yet_available": "Noch nicht verfügbar",
|
"hud.social.not_yet_available": "Noch nicht verfügbar",
|
||||||
@ -314,6 +316,23 @@ magischen Gegenstände ergattern?"#,
|
|||||||
"hud.social.play_online_fmt": "{nb_player} Spieler online",
|
"hud.social.play_online_fmt": "{nb_player} Spieler online",
|
||||||
|
|
||||||
"hud.spell": "Zauber",
|
"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": "Herstellen",
|
||||||
"hud.crafting.recipes": "Rezepte",
|
"hud.crafting.recipes": "Rezepte",
|
||||||
@ -376,6 +395,9 @@ magischen Gegenstände ergattern?"#,
|
|||||||
"gameinput.freelook": "Freie Sicht",
|
"gameinput.freelook": "Freie Sicht",
|
||||||
"gameinput.autowalk": "Automatisch Laufen",
|
"gameinput.autowalk": "Automatisch Laufen",
|
||||||
"gameinput.dance": "Tanzen",
|
"gameinput.dance": "Tanzen",
|
||||||
|
"gameinput.declinegroupinvite": "Ablehnen",
|
||||||
|
"gameinput.acceptgroupinvite": "Annehmen",
|
||||||
|
"gameinput.select": "Auswählen",
|
||||||
|
|
||||||
/// End GameInput section
|
/// End GameInput section
|
||||||
|
|
||||||
|
@ -65,11 +65,13 @@ VoxygenLocalization(
|
|||||||
"common.create": "Create",
|
"common.create": "Create",
|
||||||
"common.okay": "Okay",
|
"common.okay": "Okay",
|
||||||
"common.accept": "Accept",
|
"common.accept": "Accept",
|
||||||
|
"common.decline": "Decline",
|
||||||
"common.disclaimer": "Disclaimer",
|
"common.disclaimer": "Disclaimer",
|
||||||
"common.cancel": "Cancel",
|
"common.cancel": "Cancel",
|
||||||
"common.none": "None",
|
"common.none": "None",
|
||||||
"common.error": "Error",
|
"common.error": "Error",
|
||||||
"common.fatal_error": "Fatal Error",
|
"common.fatal_error": "Fatal Error",
|
||||||
|
"common.you": "You",
|
||||||
|
|
||||||
// Message when connection to the server is lost
|
// Message when connection to the server is lost
|
||||||
"common.connection_lost": r#"Connection lost!
|
"common.connection_lost": r#"Connection lost!
|
||||||
@ -306,12 +308,16 @@ magically infused items?"#,
|
|||||||
"hud.settings.unbound": "None",
|
"hud.settings.unbound": "None",
|
||||||
"hud.settings.reset_keybinds": "Reset to Defaults",
|
"hud.settings.reset_keybinds": "Reset to Defaults",
|
||||||
|
|
||||||
"hud.social": "Social",
|
"hud.social": "Other Players",
|
||||||
"hud.social.online": "Online",
|
"hud.social.online": "Online:",
|
||||||
"hud.social.friends": "Friends",
|
"hud.social.friends": "Friends",
|
||||||
"hud.social.not_yet_available": "Not yet available",
|
"hud.social.not_yet_available": "Not yet available",
|
||||||
"hud.social.faction": "Faction",
|
"hud.social.faction": "Faction",
|
||||||
"hud.social.play_online_fmt": "{nb_player} player(s) online",
|
"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": "Crafting",
|
||||||
"hud.crafting.recipes": "Recipes",
|
"hud.crafting.recipes": "Recipes",
|
||||||
@ -319,7 +325,20 @@ magically infused items?"#,
|
|||||||
"hud.crafting.craft": "Craft",
|
"hud.crafting.craft": "Craft",
|
||||||
"hud.crafting.tool_cata": "Requires:",
|
"hud.crafting.tool_cata": "Requires:",
|
||||||
|
|
||||||
"hud.spell": "Spells",
|
"hud.group": "Group",
|
||||||
|
"hud.group.invite_to_join": "{name} invited you to their group!",
|
||||||
|
"hud.group.invite": "Invite",
|
||||||
|
"hud.group.kick": "Kick",
|
||||||
|
"hud.group.assign_leader": "Assign Leader",
|
||||||
|
"hud.group.leave": "Leave Group",
|
||||||
|
"hud.group.dead" : "Dead",
|
||||||
|
"hud.group.out_of_range": "Out of range",
|
||||||
|
"hud.group.add_friend": "Add to Friends",
|
||||||
|
"hud.group.link_group": "Link Groups",
|
||||||
|
"hud.group.in_menu": "In Menu",
|
||||||
|
"hud.group.members": "Group Members",
|
||||||
|
|
||||||
|
"hud.spell": "Spells",
|
||||||
|
|
||||||
"hud.free_look_indicator": "Free look active",
|
"hud.free_look_indicator": "Free look active",
|
||||||
"hud.auto_walk_indicator": "Auto walk active",
|
"hud.auto_walk_indicator": "Auto walk active",
|
||||||
@ -377,7 +396,10 @@ magically infused items?"#,
|
|||||||
"gameinput.freelook": "Free Look",
|
"gameinput.freelook": "Free Look",
|
||||||
"gameinput.autowalk": "Auto Walk",
|
"gameinput.autowalk": "Auto Walk",
|
||||||
"gameinput.dance": "Dance",
|
"gameinput.dance": "Dance",
|
||||||
|
"gameinput.select": "Select Entity",
|
||||||
|
"gameinput.acceptgroupinvite": "Accept Group Invite",
|
||||||
|
"gameinput.declinegroupinvite": "Decline Group Invite",
|
||||||
|
|
||||||
/// End GameInput section
|
/// End GameInput section
|
||||||
|
|
||||||
|
|
||||||
@ -436,7 +458,8 @@ Protection
|
|||||||
"Press 'F1' to see all default keybindings.",
|
"Press 'F1' to see all default keybindings.",
|
||||||
"You can type /say or /s to only chat with players directly around you.",
|
"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.",
|
"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.",
|
"NPCs with the same level can have a different difficulty.",
|
||||||
"Look at the ground for food, chests and other loot!",
|
"Look at the ground for food, chests and other loot!",
|
||||||
"Inventory filled with food? Try crafting better food from it!",
|
"Inventory filled with food? Try crafting better food from it!",
|
||||||
@ -447,7 +470,9 @@ Protection
|
|||||||
"Press 'J' to dance. Party!",
|
"Press 'J' to dance. Party!",
|
||||||
"Press 'L-Shift' to open your Glider and conquer the skies.",
|
"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!",
|
"Veloren is still in Pre-Alpha. We do our best to improve it every day!",
|
||||||
"If you want to join the Dev-Team or just have a chat with us join our Discord-Server.",
|
"If you want to join the Dev-Team or just have a chat with us join our Discord-Server.",
|
||||||
|
"You can toggle showing your amount of health on the healthbar in the settings.",
|
||||||
|
"In order to see your stats click the 'Stats' button in your inventory.",
|
||||||
],
|
],
|
||||||
"npc.speech.villager_under_attack": [
|
"npc.speech.villager_under_attack": [
|
||||||
"Help, I'm under attack!",
|
"Help, I'm under attack!",
|
||||||
|
@ -1016,6 +1016,10 @@
|
|||||||
Armor(Back("Short0")): VoxTrans(
|
Armor(Back("Short0")): VoxTrans(
|
||||||
"voxel.armor.back.short-0",
|
"voxel.armor.back.short-0",
|
||||||
(0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.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(
|
Armor(Back("Admin")): VoxTrans(
|
||||||
"voxel.armor.back.admin",
|
"voxel.armor.back.admin",
|
||||||
@ -1033,6 +1037,9 @@
|
|||||||
Armor(Neck("Neck0")): Png(
|
Armor(Neck("Neck0")): Png(
|
||||||
"element.icons.neck-0",
|
"element.icons.neck-0",
|
||||||
),
|
),
|
||||||
|
Armor(Neck("Neck1")): Png(
|
||||||
|
"element.icons.neck-1",
|
||||||
|
),
|
||||||
// Tabards
|
// Tabards
|
||||||
Armor(Tabard("Admin")): Png(
|
Armor(Tabard("Admin")): Png(
|
||||||
"element.icons.tabard_admin",
|
"element.icons.tabard_admin",
|
||||||
|
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.
@ -15,6 +15,10 @@
|
|||||||
"DungPurp0": (
|
"DungPurp0": (
|
||||||
vox_spec: ("armor.back.dung_purp-0", (-5.0, -1.0, -14.0)),
|
vox_spec: ("armor.back.dung_purp-0", (-5.0, -1.0, -14.0)),
|
||||||
color: None
|
color: None
|
||||||
),
|
),
|
||||||
|
"Short1": (
|
||||||
|
vox_spec: ("armor.back.short-1", (-5.0, -1.0, -11.0)),
|
||||||
|
color: None
|
||||||
|
),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
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,13 +17,13 @@ use byteorder::{ByteOrder, LittleEndian};
|
|||||||
use common::{
|
use common::{
|
||||||
character::CharacterItem,
|
character::CharacterItem,
|
||||||
comp::{
|
comp::{
|
||||||
self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip,
|
self, group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
|
||||||
InventoryUpdateEvent,
|
InventoryManip, InventoryUpdateEvent,
|
||||||
},
|
},
|
||||||
msg::{
|
msg::{
|
||||||
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification,
|
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, InviteAnswer,
|
||||||
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg,
|
Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo,
|
||||||
MAX_BYTES_CHAT_MSG,
|
ServerMsg, MAX_BYTES_CHAT_MSG,
|
||||||
},
|
},
|
||||||
recipe::RecipeBook,
|
recipe::RecipeBook,
|
||||||
state::State,
|
state::State,
|
||||||
@ -79,6 +79,15 @@ pub struct Client {
|
|||||||
recipe_book: RecipeBook,
|
recipe_book: RecipeBook,
|
||||||
available_recipes: HashSet<String>,
|
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,
|
_network: Network,
|
||||||
participant: Option<Participant>,
|
participant: Option<Participant>,
|
||||||
singleton_stream: Stream,
|
singleton_stream: Stream,
|
||||||
@ -126,47 +135,49 @@ impl Client {
|
|||||||
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
|
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
|
||||||
|
|
||||||
// Wait for initial sync
|
// Wait for initial sync
|
||||||
let (state, entity, server_info, world_map, recipe_book) = block_on(async {
|
let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(
|
||||||
loop {
|
async {
|
||||||
match stream.recv().await? {
|
loop {
|
||||||
ServerMsg::InitialSync {
|
match stream.recv().await? {
|
||||||
entity_package,
|
ServerMsg::InitialSync {
|
||||||
server_info,
|
entity_package,
|
||||||
time_of_day,
|
server_info,
|
||||||
world_map: (map_size, world_map),
|
time_of_day,
|
||||||
recipe_book,
|
max_group_size,
|
||||||
} => {
|
world_map: (map_size, world_map),
|
||||||
// TODO: Display that versions don't match in Voxygen
|
recipe_book,
|
||||||
if &server_info.git_hash != *common::util::GIT_HASH {
|
} => {
|
||||||
warn!(
|
// TODO: Display that versions don't match in Voxygen
|
||||||
"Server is running {}[{}], you are running {}[{}], versions might \
|
if &server_info.git_hash != *common::util::GIT_HASH {
|
||||||
be incompatible!",
|
warn!(
|
||||||
server_info.git_hash,
|
"Server is running {}[{}], you are running {}[{}], versions \
|
||||||
server_info.git_date,
|
might be incompatible!",
|
||||||
common::util::GIT_HASH.to_string(),
|
server_info.git_hash,
|
||||||
common::util::GIT_DATE.to_string(),
|
server_info.git_date,
|
||||||
);
|
common::util::GIT_HASH.to_string(),
|
||||||
}
|
common::util::GIT_DATE.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
debug!("Auth Server: {:?}", server_info.auth_provider);
|
debug!("Auth Server: {:?}", server_info.auth_provider);
|
||||||
|
|
||||||
// Initialize `State`
|
// Initialize `State`
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
// Client-only components
|
// Client-only components
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
.register::<comp::Last<comp::CharacterState>>();
|
.register::<comp::Last<comp::CharacterState>>();
|
||||||
|
|
||||||
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
||||||
*state.ecs_mut().write_resource() = time_of_day;
|
*state.ecs_mut().write_resource() = time_of_day;
|
||||||
|
|
||||||
assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize);
|
assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize);
|
||||||
let mut world_map_raw =
|
let mut world_map_raw =
|
||||||
vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/];
|
vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/];
|
||||||
LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
|
LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
|
||||||
debug!("Preparing image...");
|
debug!("Preparing image...");
|
||||||
let world_map = Arc::new(
|
let world_map = Arc::new(
|
||||||
image::DynamicImage::ImageRgba8({
|
image::DynamicImage::ImageRgba8({
|
||||||
// Should not fail if the dimensions are correct.
|
// Should not fail if the dimensions are correct.
|
||||||
let world_map =
|
let world_map =
|
||||||
image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw);
|
image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw);
|
||||||
@ -175,24 +186,26 @@ impl Client {
|
|||||||
// Flip the image, since Voxygen uses an orientation where rotation from
|
// Flip the image, since Voxygen uses an orientation where rotation from
|
||||||
// positive x axis to positive y axis is counterclockwise around the z axis.
|
// positive x axis to positive y axis is counterclockwise around the z axis.
|
||||||
.flipv(),
|
.flipv(),
|
||||||
);
|
);
|
||||||
debug!("Done preparing image...");
|
debug!("Done preparing image...");
|
||||||
|
|
||||||
break Ok((
|
break Ok((
|
||||||
state,
|
state,
|
||||||
entity,
|
entity,
|
||||||
server_info,
|
server_info,
|
||||||
(world_map, map_size),
|
(world_map, map_size),
|
||||||
recipe_book,
|
recipe_book,
|
||||||
));
|
max_group_size,
|
||||||
},
|
));
|
||||||
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
|
},
|
||||||
err => {
|
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
|
||||||
warn!("whoops, server mad {:?}, ignoring", err);
|
err => {
|
||||||
},
|
warn!("whoops, server mad {:?}, ignoring", err);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})?;
|
)?;
|
||||||
|
|
||||||
stream.send(ClientMsg::Ping)?;
|
stream.send(ClientMsg::Ping)?;
|
||||||
|
|
||||||
@ -213,6 +226,12 @@ impl Client {
|
|||||||
recipe_book,
|
recipe_book,
|
||||||
available_recipes: HashSet::default(),
|
available_recipes: HashSet::default(),
|
||||||
|
|
||||||
|
max_group_size,
|
||||||
|
group_invite: None,
|
||||||
|
group_leader: None,
|
||||||
|
group_members: HashMap::new(),
|
||||||
|
pending_invites: HashSet::new(),
|
||||||
|
|
||||||
_network: network,
|
_network: network,
|
||||||
participant: Some(participant),
|
participant: Some(participant),
|
||||||
singleton_stream: stream,
|
singleton_stream: stream,
|
||||||
@ -375,7 +394,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn pick_up(&mut self, entity: EcsEntity) {
|
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
|
self.singleton_stream
|
||||||
.send(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
|
.send(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
|
||||||
InventoryManip::Pickup(uid),
|
InventoryManip::Pickup(uid),
|
||||||
@ -424,6 +443,72 @@ impl Client {
|
|||||||
.unwrap();
|
.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 {
|
pub fn is_mounted(&self) -> bool {
|
||||||
self.state
|
self.state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -433,7 +518,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mount(&mut self, entity: EcsEntity) {
|
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
|
self.singleton_stream
|
||||||
.send(ClientMsg::ControlEvent(ControlEvent::Mount(uid)))
|
.send(ClientMsg::ControlEvent(ControlEvent::Mount(uid)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -690,6 +775,13 @@ impl Client {
|
|||||||
frontend_events.append(&mut self.handle_new_messages()?);
|
frontend_events.append(&mut self.handle_new_messages()?);
|
||||||
|
|
||||||
// 3) Update client local data
|
// 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
|
// 4) Tick the client's LocalState
|
||||||
self.state.tick(dt, add_foreign_systems, true);
|
self.state.tick(dt, add_foreign_systems, true);
|
||||||
@ -935,7 +1027,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 => {
|
ServerMsg::Ping => {
|
||||||
self.singleton_stream.send(ClientMsg::Pong)?;
|
self.singleton_stream.send(ClientMsg::Pong)?;
|
||||||
},
|
},
|
||||||
@ -976,7 +1163,7 @@ impl Client {
|
|||||||
self.state.ecs_mut().apply_entity_package(entity_package);
|
self.state.ecs_mut().apply_entity_package(entity_package);
|
||||||
},
|
},
|
||||||
ServerMsg::DeleteEntity(entity) => {
|
ServerMsg::DeleteEntity(entity) => {
|
||||||
if self.state.read_component_cloned::<Uid>(self.entity) != Some(entity) {
|
if self.uid() != Some(entity) {
|
||||||
self.state
|
self.state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
.delete_entity_and_clear_from_uid_allocator(entity.0);
|
.delete_entity_and_clear_from_uid_allocator(entity.0);
|
||||||
@ -1086,6 +1273,9 @@ impl Client {
|
|||||||
/// Get the player's entity.
|
/// Get the player's entity.
|
||||||
pub fn entity(&self) -> EcsEntity { self.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
|
/// Get the client state
|
||||||
pub fn get_client_state(&self) -> ClientState { self.client_state }
|
pub fn get_client_state(&self) -> ClientState { self.client_state }
|
||||||
|
|
||||||
@ -1137,7 +1327,7 @@ impl Client {
|
|||||||
pub fn is_admin(&self) -> bool {
|
pub fn is_admin(&self) -> bool {
|
||||||
let client_uid = self
|
let client_uid = self
|
||||||
.state
|
.state
|
||||||
.read_component_cloned::<Uid>(self.entity)
|
.read_component_copied::<Uid>(self.entity)
|
||||||
.expect("Client doesn't have a Uid!!!");
|
.expect("Client doesn't have a Uid!!!");
|
||||||
|
|
||||||
self.player_list
|
self.player_list
|
||||||
@ -1148,8 +1338,7 @@ impl Client {
|
|||||||
/// Clean client ECS state
|
/// Clean client ECS state
|
||||||
fn clean_state(&mut self) {
|
fn clean_state(&mut self) {
|
||||||
let client_uid = self
|
let client_uid = self
|
||||||
.state
|
.uid()
|
||||||
.read_component_cloned::<Uid>(self.entity)
|
|
||||||
.map(|u| u.into())
|
.map(|u| u.into())
|
||||||
.expect("Client doesn't have a Uid!!!");
|
.expect("Client doesn't have a Uid!!!");
|
||||||
|
|
||||||
@ -1220,7 +1409,7 @@ impl Client {
|
|||||||
comp::ChatType::Tell(from, to) => {
|
comp::ChatType::Tell(from, to) => {
|
||||||
let from_alias = alias_of_uid(from);
|
let from_alias = alias_of_uid(from);
|
||||||
let to_alias = alias_of_uid(to);
|
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)
|
format!("To [{}]: {}", to_alias, message)
|
||||||
} else {
|
} else {
|
||||||
format!("From [{}]: {}", from_alias, message)
|
format!("From [{}]: {}", from_alias, message)
|
||||||
|
@ -30,6 +30,7 @@ notify = "5.0.0-pre.3"
|
|||||||
indexmap = "1.3.0"
|
indexmap = "1.3.0"
|
||||||
sum_type = "0.2.0"
|
sum_type = "0.2.0"
|
||||||
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" }
|
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" }
|
||||||
|
slab = "0.4.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
|
@ -50,7 +50,6 @@ pub enum ChatCommand {
|
|||||||
Health,
|
Health,
|
||||||
Help,
|
Help,
|
||||||
JoinFaction,
|
JoinFaction,
|
||||||
JoinGroup,
|
|
||||||
Jump,
|
Jump,
|
||||||
Kill,
|
Kill,
|
||||||
KillNpcs,
|
KillNpcs,
|
||||||
@ -92,7 +91,6 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
|
|||||||
ChatCommand::Health,
|
ChatCommand::Health,
|
||||||
ChatCommand::Help,
|
ChatCommand::Help,
|
||||||
ChatCommand::JoinFaction,
|
ChatCommand::JoinFaction,
|
||||||
ChatCommand::JoinGroup,
|
|
||||||
ChatCommand::Jump,
|
ChatCommand::Jump,
|
||||||
ChatCommand::Kill,
|
ChatCommand::Kill,
|
||||||
ChatCommand::KillNpcs,
|
ChatCommand::KillNpcs,
|
||||||
@ -246,11 +244,6 @@ impl ChatCommand {
|
|||||||
"Join/leave the specified faction",
|
"Join/leave the specified faction",
|
||||||
NoAdmin,
|
NoAdmin,
|
||||||
),
|
),
|
||||||
ChatCommand::JoinGroup => ChatCommandData::new(
|
|
||||||
vec![Any("group", Optional)],
|
|
||||||
"Join/leave the specified group",
|
|
||||||
NoAdmin,
|
|
||||||
),
|
|
||||||
ChatCommand::Jump => cmd(
|
ChatCommand::Jump => cmd(
|
||||||
vec![
|
vec![
|
||||||
Float("x", 0.0, Required),
|
Float("x", 0.0, Required),
|
||||||
@ -383,7 +376,6 @@ impl ChatCommand {
|
|||||||
ChatCommand::Group => "group",
|
ChatCommand::Group => "group",
|
||||||
ChatCommand::Health => "health",
|
ChatCommand::Health => "health",
|
||||||
ChatCommand::JoinFaction => "join_faction",
|
ChatCommand::JoinFaction => "join_faction",
|
||||||
ChatCommand::JoinGroup => "join_group",
|
|
||||||
ChatCommand::Help => "help",
|
ChatCommand::Help => "help",
|
||||||
ChatCommand::Jump => "jump",
|
ChatCommand::Jump => "jump",
|
||||||
ChatCommand::Kill => "kill",
|
ChatCommand::Kill => "kill",
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use crate::{path::Chaser, sync::Uid};
|
use crate::{path::Chaser, sync::Uid};
|
||||||
use serde::{Deserialize, Serialize};
|
use specs::{Component, Entity as EcsEntity};
|
||||||
use specs::{Component, Entity as EcsEntity, FlaggedStorage};
|
|
||||||
use specs_idvs::IdvStorage;
|
use specs_idvs::IdvStorage;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum Alignment {
|
pub enum Alignment {
|
||||||
/// Wild animals and gentle giants
|
/// Wild animals and gentle giants
|
||||||
Wild,
|
Wild,
|
||||||
@ -52,7 +51,7 @@ impl Alignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Alignment {
|
impl Component for Alignment {
|
||||||
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
|
type Storage = IdvStorage<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{msg::ServerMsg, sync::Uid};
|
use crate::{comp::group::Group, msg::ServerMsg, sync::Uid};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specs::Component;
|
use specs::Component;
|
||||||
use specs_idvs::IdvStorage;
|
use specs_idvs::IdvStorage;
|
||||||
@ -15,7 +15,7 @@ pub enum ChatMode {
|
|||||||
/// Talk to players in your region of the world
|
/// Talk to players in your region of the world
|
||||||
Region,
|
Region,
|
||||||
/// Talk to your current group of players
|
/// Talk to your current group of players
|
||||||
Group(String),
|
Group(Group),
|
||||||
/// Talk to your faction
|
/// Talk to your faction
|
||||||
Faction(String),
|
Faction(String),
|
||||||
/// Talk to every player on the server
|
/// Talk to every player on the server
|
||||||
@ -28,16 +28,16 @@ impl Component for ChatMode {
|
|||||||
|
|
||||||
impl ChatMode {
|
impl ChatMode {
|
||||||
/// Create a message from your current chat mode and uuid.
|
/// 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 {
|
let chat_type = match self {
|
||||||
ChatMode::Tell(to) => ChatType::Tell(from, *to),
|
ChatMode::Tell(to) => ChatType::Tell(from, *to),
|
||||||
ChatMode::Say => ChatType::Say(from),
|
ChatMode::Say => ChatType::Say(from),
|
||||||
ChatMode::Region => ChatType::Region(from),
|
ChatMode::Region => ChatType::Region(from),
|
||||||
ChatMode::Group(name) => ChatType::Group(from, name.to_string()),
|
ChatMode::Group(group) => ChatType::Group(from, *group),
|
||||||
ChatMode::Faction(name) => ChatType::Faction(from, name.to_string()),
|
ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()),
|
||||||
ChatMode::World => ChatType::World(from),
|
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`
|
/// This is a superset of `SpeechBubbleType`, which is a superset of `ChatMode`
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum ChatType {
|
pub enum ChatType<G> {
|
||||||
/// A player came online
|
/// A player came online
|
||||||
Online,
|
Online,
|
||||||
/// A player went offline
|
/// A player went offline
|
||||||
@ -61,7 +61,7 @@ pub enum ChatType {
|
|||||||
/// Inform players that someone died
|
/// Inform players that someone died
|
||||||
Kill,
|
Kill,
|
||||||
/// Server notifications to a group, such as player join/leave
|
/// Server notifications to a group, such as player join/leave
|
||||||
GroupMeta(String),
|
GroupMeta(G),
|
||||||
/// Server notifications to a faction, such as player join/leave
|
/// Server notifications to a faction, such as player join/leave
|
||||||
FactionMeta(String),
|
FactionMeta(String),
|
||||||
/// One-on-one chat (from, to)
|
/// One-on-one chat (from, to)
|
||||||
@ -69,7 +69,7 @@ pub enum ChatType {
|
|||||||
/// Chat with nearby players
|
/// Chat with nearby players
|
||||||
Say(Uid),
|
Say(Uid),
|
||||||
/// Group chat
|
/// Group chat
|
||||||
Group(Uid, String),
|
Group(Uid, G),
|
||||||
/// Factional chat
|
/// Factional chat
|
||||||
Faction(Uid, String),
|
Faction(Uid, String),
|
||||||
/// Regional chat
|
/// Regional chat
|
||||||
@ -86,17 +86,18 @@ pub enum ChatType {
|
|||||||
Loot,
|
Loot,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatType {
|
impl<G> ChatType<G> {
|
||||||
pub fn chat_msg<S>(self, msg: S) -> ChatMsg
|
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G>
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
{
|
{
|
||||||
ChatMsg {
|
GenericChatMsg {
|
||||||
chat_type: self,
|
chat_type: self,
|
||||||
message: msg.into(),
|
message: msg.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
impl ChatType<String> {
|
||||||
pub fn server_msg<S>(self, msg: S) -> ServerMsg
|
pub fn server_msg<S>(self, msg: S) -> ServerMsg
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
@ -106,12 +107,15 @@ impl ChatType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ChatMsg {
|
pub struct GenericChatMsg<G> {
|
||||||
pub chat_type: ChatType,
|
pub chat_type: ChatType<G>,
|
||||||
pub message: String,
|
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 NPC_DISTANCE: f32 = 100.0;
|
||||||
pub const REGION_DISTANCE: f32 = 1000.0;
|
pub const REGION_DISTANCE: f32 = 1000.0;
|
||||||
pub const SAY_DISTANCE: f32 = 100.0;
|
pub const SAY_DISTANCE: f32 = 100.0;
|
||||||
@ -121,6 +125,32 @@ impl ChatMsg {
|
|||||||
Self { chat_type, message }
|
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)> {
|
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
|
||||||
let icon = self.icon();
|
let icon = self.icon();
|
||||||
if let ChatType::Npc(from, r) = self.chat_type {
|
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
|
/// Player factions are used to coordinate pvp vs hostile factions or segment
|
||||||
/// chat from the world
|
/// chat from the world
|
||||||
///
|
///
|
||||||
|
@ -18,12 +18,23 @@ pub enum InventoryManip {
|
|||||||
CraftRecipe(String),
|
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)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ControlEvent {
|
pub enum ControlEvent {
|
||||||
ToggleLantern,
|
ToggleLantern,
|
||||||
Mount(Uid),
|
Mount(Uid),
|
||||||
Unmount,
|
Unmount,
|
||||||
InventoryManip(InventoryManip),
|
InventoryManip(InventoryManip),
|
||||||
|
GroupManip(GroupManip),
|
||||||
Respawn,
|
Respawn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ mod chat;
|
|||||||
mod controller;
|
mod controller;
|
||||||
mod damage;
|
mod damage;
|
||||||
mod energy;
|
mod energy;
|
||||||
|
pub mod group;
|
||||||
mod inputs;
|
mod inputs;
|
||||||
mod inventory;
|
mod inventory;
|
||||||
mod last;
|
mod last;
|
||||||
@ -28,13 +29,16 @@ pub use body::{
|
|||||||
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
|
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
|
||||||
};
|
};
|
||||||
pub use character_state::{Attacking, CharacterState, StateUpdate};
|
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::{
|
pub use controller::{
|
||||||
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip,
|
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
|
||||||
MountState, Mounting,
|
InventoryManip, MountState, Mounting,
|
||||||
};
|
};
|
||||||
pub use damage::{Damage, DamageSource};
|
pub use damage::{Damage, DamageSource};
|
||||||
pub use energy::{Energy, EnergySource};
|
pub use energy::{Energy, EnergySource};
|
||||||
|
pub use group::Group;
|
||||||
pub use inputs::CanBuild;
|
pub use inputs::CanBuild;
|
||||||
pub use inventory::{
|
pub use inventory::{
|
||||||
item,
|
item,
|
||||||
|
@ -25,6 +25,7 @@ pub enum ServerEvent {
|
|||||||
pos: Vec3<f32>,
|
pos: Vec3<f32>,
|
||||||
power: f32,
|
power: f32,
|
||||||
owner: Option<Uid>,
|
owner: Option<Uid>,
|
||||||
|
friendly_damage: bool,
|
||||||
},
|
},
|
||||||
Damage {
|
Damage {
|
||||||
uid: Uid,
|
uid: Uid,
|
||||||
@ -35,6 +36,7 @@ pub enum ServerEvent {
|
|||||||
cause: comp::HealthSource,
|
cause: comp::HealthSource,
|
||||||
},
|
},
|
||||||
InventoryManip(EcsEntity, comp::InventoryManip),
|
InventoryManip(EcsEntity, comp::InventoryManip),
|
||||||
|
GroupManip(EcsEntity, comp::GroupManip),
|
||||||
Respawn(EcsEntity),
|
Respawn(EcsEntity),
|
||||||
Shoot {
|
Shoot {
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
@ -80,7 +82,7 @@ pub enum ServerEvent {
|
|||||||
ChunkRequest(EcsEntity, Vec2<i32>),
|
ChunkRequest(EcsEntity, Vec2<i32>),
|
||||||
ChatCmd(EcsEntity, String),
|
ChatCmd(EcsEntity, String),
|
||||||
/// Send a chat message to the player from an npc or other player
|
/// Send a chat message to the player from an npc or other player
|
||||||
Chat(comp::ChatMsg),
|
Chat(comp::UnresolvedChatMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventBus<E> {
|
pub struct EventBus<E> {
|
||||||
|
@ -17,7 +17,7 @@ sum_type! {
|
|||||||
LightEmitter(comp::LightEmitter),
|
LightEmitter(comp::LightEmitter),
|
||||||
Item(comp::Item),
|
Item(comp::Item),
|
||||||
Scale(comp::Scale),
|
Scale(comp::Scale),
|
||||||
Alignment(comp::Alignment),
|
Group(comp::Group),
|
||||||
MountState(comp::MountState),
|
MountState(comp::MountState),
|
||||||
Mounting(comp::Mounting),
|
Mounting(comp::Mounting),
|
||||||
Mass(comp::Mass),
|
Mass(comp::Mass),
|
||||||
@ -44,7 +44,7 @@ sum_type! {
|
|||||||
LightEmitter(PhantomData<comp::LightEmitter>),
|
LightEmitter(PhantomData<comp::LightEmitter>),
|
||||||
Item(PhantomData<comp::Item>),
|
Item(PhantomData<comp::Item>),
|
||||||
Scale(PhantomData<comp::Scale>),
|
Scale(PhantomData<comp::Scale>),
|
||||||
Alignment(PhantomData<comp::Alignment>),
|
Group(PhantomData<comp::Group>),
|
||||||
MountState(PhantomData<comp::MountState>),
|
MountState(PhantomData<comp::MountState>),
|
||||||
Mounting(PhantomData<comp::Mounting>),
|
Mounting(PhantomData<comp::Mounting>),
|
||||||
Mass(PhantomData<comp::Mass>),
|
Mass(PhantomData<comp::Mass>),
|
||||||
@ -71,7 +71,7 @@ impl sync::CompPacket for EcsCompPacket {
|
|||||||
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
|
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
|
||||||
EcsCompPacket::Item(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::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::MountState(comp) => sync::handle_insert(comp, entity, world),
|
||||||
EcsCompPacket::Mounting(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),
|
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::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
|
||||||
EcsCompPacket::Item(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::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::MountState(comp) => sync::handle_modify(comp, entity, world),
|
||||||
EcsCompPacket::Mounting(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),
|
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::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
|
||||||
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(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::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
|
||||||
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
|
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
|
||||||
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
|
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
|
||||||
|
@ -7,7 +7,7 @@ pub use self::{
|
|||||||
client::ClientMsg,
|
client::ClientMsg,
|
||||||
ecs_packet::EcsCompPacket,
|
ecs_packet::EcsCompPacket,
|
||||||
server::{
|
server::{
|
||||||
CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
|
CharacterInfo, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
|
||||||
RequestStateError, ServerInfo, ServerMsg,
|
RequestStateError, ServerInfo, ServerMsg,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,13 @@ pub struct CharacterInfo {
|
|||||||
pub level: u32,
|
pub level: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum InviteAnswer {
|
||||||
|
Accepted,
|
||||||
|
Declined,
|
||||||
|
TimedOut,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum Notification {
|
pub enum Notification {
|
||||||
WaypointSaved,
|
WaypointSaved,
|
||||||
@ -59,6 +66,7 @@ pub enum ServerMsg {
|
|||||||
entity_package: sync::EntityPackage<EcsCompPacket>,
|
entity_package: sync::EntityPackage<EcsCompPacket>,
|
||||||
server_info: ServerInfo,
|
server_info: ServerInfo,
|
||||||
time_of_day: state::TimeOfDay,
|
time_of_day: state::TimeOfDay,
|
||||||
|
max_group_size: u32,
|
||||||
world_map: (Vec2<u32>, Vec<u32>),
|
world_map: (Vec2<u32>, Vec<u32>),
|
||||||
recipe_book: RecipeBook,
|
recipe_book: RecipeBook,
|
||||||
},
|
},
|
||||||
@ -69,6 +77,22 @@ pub enum ServerMsg {
|
|||||||
/// An error occured while creating or deleting a character
|
/// An error occured while creating or deleting a character
|
||||||
CharacterActionError(String),
|
CharacterActionError(String),
|
||||||
PlayerListUpdate(PlayerListUpdate),
|
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)>),
|
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
||||||
/// Trigger cleanup for when the client goes back to the `Registered` state
|
/// Trigger cleanup for when the client goes back to the `Registered` state
|
||||||
/// from an ingame state
|
/// from an ingame state
|
||||||
|
@ -123,7 +123,7 @@ impl State {
|
|||||||
ecs.register::<comp::Gravity>();
|
ecs.register::<comp::Gravity>();
|
||||||
ecs.register::<comp::CharacterState>();
|
ecs.register::<comp::CharacterState>();
|
||||||
ecs.register::<comp::Object>();
|
ecs.register::<comp::Object>();
|
||||||
ecs.register::<comp::Alignment>();
|
ecs.register::<comp::Group>();
|
||||||
|
|
||||||
// Register components send from clients -> server
|
// Register components send from clients -> server
|
||||||
ecs.register::<comp::Controller>();
|
ecs.register::<comp::Controller>();
|
||||||
@ -146,6 +146,7 @@ impl State {
|
|||||||
ecs.register::<comp::Last<comp::Pos>>();
|
ecs.register::<comp::Last<comp::Pos>>();
|
||||||
ecs.register::<comp::Last<comp::Vel>>();
|
ecs.register::<comp::Last<comp::Vel>>();
|
||||||
ecs.register::<comp::Last<comp::Ori>>();
|
ecs.register::<comp::Last<comp::Ori>>();
|
||||||
|
ecs.register::<comp::Alignment>();
|
||||||
ecs.register::<comp::Agent>();
|
ecs.register::<comp::Agent>();
|
||||||
ecs.register::<comp::WaypointArea>();
|
ecs.register::<comp::WaypointArea>();
|
||||||
ecs.register::<comp::ForceUpdate>();
|
ecs.register::<comp::ForceUpdate>();
|
||||||
@ -156,8 +157,9 @@ impl State {
|
|||||||
ecs.register::<comp::Attacking>();
|
ecs.register::<comp::Attacking>();
|
||||||
ecs.register::<comp::ItemDrop>();
|
ecs.register::<comp::ItemDrop>();
|
||||||
ecs.register::<comp::ChatMode>();
|
ecs.register::<comp::ChatMode>();
|
||||||
ecs.register::<comp::Group>();
|
|
||||||
ecs.register::<comp::Faction>();
|
ecs.register::<comp::Faction>();
|
||||||
|
ecs.register::<comp::group::Invite>();
|
||||||
|
ecs.register::<comp::group::PendingInvites>();
|
||||||
|
|
||||||
// Register synced resources used by the ECS.
|
// Register synced resources used by the ECS.
|
||||||
ecs.insert(TimeOfDay(0.0));
|
ecs.insert(TimeOfDay(0.0));
|
||||||
@ -168,9 +170,10 @@ impl State {
|
|||||||
ecs.insert(TerrainGrid::new().unwrap());
|
ecs.insert(TerrainGrid::new().unwrap());
|
||||||
ecs.insert(BlockChange::default());
|
ecs.insert(BlockChange::default());
|
||||||
ecs.insert(TerrainChanges::default());
|
ecs.insert(TerrainChanges::default());
|
||||||
|
ecs.insert(EventBus::<LocalEvent>::default());
|
||||||
// TODO: only register on the server
|
// TODO: only register on the server
|
||||||
ecs.insert(EventBus::<ServerEvent>::default());
|
ecs.insert(EventBus::<ServerEvent>::default());
|
||||||
ecs.insert(EventBus::<LocalEvent>::default());
|
ecs.insert(comp::group::GroupManager::default());
|
||||||
ecs.insert(RegionMap::new());
|
ecs.insert(RegionMap::new());
|
||||||
|
|
||||||
ecs
|
ecs
|
||||||
@ -196,8 +199,8 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read a component attributed to a particular entity.
|
/// Read a component attributed to a particular entity.
|
||||||
pub fn read_component_cloned<C: Component + Clone>(&self, entity: EcsEntity) -> Option<C> {
|
pub fn read_component_copied<C: Component + Copy>(&self, entity: EcsEntity) -> Option<C> {
|
||||||
self.ecs.read_storage().get(entity).cloned()
|
self.ecs.read_storage().get(entity).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a read-only reference to the storage of a particular component type.
|
/// Get a read-only reference to the storage of a particular component type.
|
||||||
|
@ -2,9 +2,12 @@ use crate::{
|
|||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
agent::Activity,
|
agent::Activity,
|
||||||
|
group,
|
||||||
|
group::Invite,
|
||||||
item::{tool::ToolKind, ItemKind},
|
item::{tool::ToolKind, ItemKind},
|
||||||
Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout,
|
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
|
||||||
MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel,
|
GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg,
|
||||||
|
Vel,
|
||||||
},
|
},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
path::{Chaser, TraversalConfig},
|
path::{Chaser, TraversalConfig},
|
||||||
@ -29,6 +32,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
Read<'a, UidAllocator>,
|
Read<'a, UidAllocator>,
|
||||||
Read<'a, Time>,
|
Read<'a, Time>,
|
||||||
Read<'a, DeltaTime>,
|
Read<'a, DeltaTime>,
|
||||||
|
Read<'a, group::GroupManager>,
|
||||||
Write<'a, EventBus<ServerEvent>>,
|
Write<'a, EventBus<ServerEvent>>,
|
||||||
Entities<'a>,
|
Entities<'a>,
|
||||||
ReadStorage<'a, Pos>,
|
ReadStorage<'a, Pos>,
|
||||||
@ -40,12 +44,14 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadStorage<'a, CharacterState>,
|
ReadStorage<'a, CharacterState>,
|
||||||
ReadStorage<'a, PhysicsState>,
|
ReadStorage<'a, PhysicsState>,
|
||||||
ReadStorage<'a, Uid>,
|
ReadStorage<'a, Uid>,
|
||||||
|
ReadStorage<'a, group::Group>,
|
||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, TerrainGrid>,
|
||||||
ReadStorage<'a, Alignment>,
|
ReadStorage<'a, Alignment>,
|
||||||
ReadStorage<'a, Body>,
|
ReadStorage<'a, Body>,
|
||||||
WriteStorage<'a, Agent>,
|
WriteStorage<'a, Agent>,
|
||||||
WriteStorage<'a, Controller>,
|
WriteStorage<'a, Controller>,
|
||||||
ReadStorage<'a, MountState>,
|
ReadStorage<'a, MountState>,
|
||||||
|
ReadStorage<'a, Invite>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
||||||
@ -55,6 +61,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
uid_allocator,
|
uid_allocator,
|
||||||
time,
|
time,
|
||||||
dt,
|
dt,
|
||||||
|
group_manager,
|
||||||
event_bus,
|
event_bus,
|
||||||
entities,
|
entities,
|
||||||
positions,
|
positions,
|
||||||
@ -66,12 +73,14 @@ impl<'a> System<'a> for Sys {
|
|||||||
character_states,
|
character_states,
|
||||||
physics_states,
|
physics_states,
|
||||||
uids,
|
uids,
|
||||||
|
groups,
|
||||||
terrain,
|
terrain,
|
||||||
alignments,
|
alignments,
|
||||||
bodies,
|
bodies,
|
||||||
mut agents,
|
mut agents,
|
||||||
mut controllers,
|
mut controllers,
|
||||||
mount_states,
|
mount_states,
|
||||||
|
invites,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
for (
|
for (
|
||||||
@ -88,6 +97,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
agent,
|
agent,
|
||||||
controller,
|
controller,
|
||||||
mount_state,
|
mount_state,
|
||||||
|
group,
|
||||||
) in (
|
) in (
|
||||||
&entities,
|
&entities,
|
||||||
&positions,
|
&positions,
|
||||||
@ -102,9 +112,23 @@ impl<'a> System<'a> for Sys {
|
|||||||
&mut agents,
|
&mut agents,
|
||||||
&mut controllers,
|
&mut controllers,
|
||||||
mount_states.maybe(),
|
mount_states.maybe(),
|
||||||
|
groups.maybe(),
|
||||||
)
|
)
|
||||||
.join()
|
.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
|
// Skip mounted entities
|
||||||
if mount_state
|
if mount_state
|
||||||
.map(|ms| *ms != MountState::Unmounted)
|
.map(|ms| *ms != MountState::Unmounted)
|
||||||
@ -117,7 +141,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
|
|
||||||
let mut inputs = &mut controller.inputs;
|
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;
|
inputs.look_dir = ori.0;
|
||||||
|
|
||||||
const AVG_FOLLOW_DIST: f32 = 6.0;
|
const AVG_FOLLOW_DIST: f32 = 6.0;
|
||||||
@ -148,11 +172,9 @@ impl<'a> System<'a> for Sys {
|
|||||||
thread_rng().gen::<f32>() - 0.5,
|
thread_rng().gen::<f32>() - 0.5,
|
||||||
) * 0.1
|
) * 0.1
|
||||||
- *bearing * 0.003
|
- *bearing * 0.003
|
||||||
- if let Some(patrol_origin) = agent.patrol_origin {
|
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
|
||||||
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
|
(pos.0 - patrol_origin).xy() * 0.0002
|
||||||
} else {
|
});
|
||||||
Vec2::zero()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop if we're too close to a wall
|
// Stop if we're too close to a wall
|
||||||
*bearing *= 0.1
|
*bearing *= 0.1
|
||||||
@ -169,8 +191,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
.until(|block| block.is_solid())
|
.until(|block| block.is_solid())
|
||||||
.cast()
|
.cast()
|
||||||
.1
|
.1
|
||||||
.map(|b| b.is_none())
|
.map_or(true, |b| b.is_none())
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
{
|
||||||
0.9
|
0.9
|
||||||
} else {
|
} else {
|
||||||
@ -269,8 +290,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
// Don't attack entities we are passive towards
|
// Don't attack entities we are passive towards
|
||||||
// TODO: This is here, it's a bit of a hack
|
// TODO: This is here, it's a bit of a hack
|
||||||
if let Some(alignment) = alignment {
|
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;
|
do_idle = true;
|
||||||
break 'activity;
|
break 'activity;
|
||||||
}
|
}
|
||||||
@ -418,8 +438,9 @@ impl<'a> System<'a> for Sys {
|
|||||||
if stats.get(attacker).map_or(false, |a| !a.is_dead) {
|
if stats.get(attacker).map_or(false, |a| !a.is_dead) {
|
||||||
if agent.can_speak {
|
if agent.can_speak {
|
||||||
let msg = "npc.speech.villager_under_attack".to_string();
|
let msg = "npc.speech.villager_under_attack".to_string();
|
||||||
event_bus
|
event_bus.emit_now(ServerEvent::Chat(
|
||||||
.emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg)));
|
UnresolvedChatMsg::npc(*uid, msg),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.activity = Activity::Attack {
|
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
|
// 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())?;
|
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.move_dir.map(|e| !e.is_nan()).reduce_and());
|
||||||
debug_assert!(inputs.look_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comp::{
|
comp::{
|
||||||
Alignment, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange,
|
group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource,
|
||||||
HealthSource, Loadout, Ori, Pos, Scale, Stats,
|
Loadout, Ori, Pos, Scale, Stats,
|
||||||
},
|
},
|
||||||
event::{EventBus, LocalEvent, ServerEvent},
|
event::{EventBus, LocalEvent, ServerEvent},
|
||||||
sync::Uid,
|
sync::Uid,
|
||||||
@ -26,10 +26,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadStorage<'a, Pos>,
|
ReadStorage<'a, Pos>,
|
||||||
ReadStorage<'a, Ori>,
|
ReadStorage<'a, Ori>,
|
||||||
ReadStorage<'a, Scale>,
|
ReadStorage<'a, Scale>,
|
||||||
ReadStorage<'a, Alignment>,
|
|
||||||
ReadStorage<'a, Body>,
|
ReadStorage<'a, Body>,
|
||||||
ReadStorage<'a, Stats>,
|
ReadStorage<'a, Stats>,
|
||||||
ReadStorage<'a, Loadout>,
|
ReadStorage<'a, Loadout>,
|
||||||
|
ReadStorage<'a, group::Group>,
|
||||||
WriteStorage<'a, Attacking>,
|
WriteStorage<'a, Attacking>,
|
||||||
WriteStorage<'a, CharacterState>,
|
WriteStorage<'a, CharacterState>,
|
||||||
);
|
);
|
||||||
@ -44,10 +44,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
positions,
|
positions,
|
||||||
orientations,
|
orientations,
|
||||||
scales,
|
scales,
|
||||||
alignments,
|
|
||||||
bodies,
|
bodies,
|
||||||
stats,
|
stats,
|
||||||
loadouts,
|
loadouts,
|
||||||
|
groups,
|
||||||
mut attacking_storage,
|
mut attacking_storage,
|
||||||
character_states,
|
character_states,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
@ -71,23 +71,12 @@ impl<'a> System<'a> for Sys {
|
|||||||
attack.applied = true;
|
attack.applied = true;
|
||||||
|
|
||||||
// Go through all other entities
|
// Go through all other entities
|
||||||
for (
|
for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in (
|
||||||
b,
|
|
||||||
uid_b,
|
|
||||||
pos_b,
|
|
||||||
ori_b,
|
|
||||||
scale_b_maybe,
|
|
||||||
alignment_b_maybe,
|
|
||||||
character_b,
|
|
||||||
stats_b,
|
|
||||||
body_b,
|
|
||||||
) in (
|
|
||||||
&entities,
|
&entities,
|
||||||
&uids,
|
&uids,
|
||||||
&positions,
|
&positions,
|
||||||
&orientations,
|
&orientations,
|
||||||
scales.maybe(),
|
scales.maybe(),
|
||||||
alignments.maybe(),
|
|
||||||
character_states.maybe(),
|
character_states.maybe(),
|
||||||
&stats,
|
&stats,
|
||||||
&bodies,
|
&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)
|
&& 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()
|
&& 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
|
// Weapon gives base damage
|
||||||
let source = if attack.base_healthchange > 0 {
|
let source = if attack.base_healthchange > 0 {
|
||||||
DamageSource::Healing
|
DamageSource::Healing
|
||||||
@ -121,28 +121,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
healthchange: attack.base_healthchange as f32,
|
healthchange: attack.base_healthchange as f32,
|
||||||
source,
|
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)
|
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;
|
&& 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 {
|
local_emitter.emit(LocalEvent::ApplyForce {
|
||||||
entity: b,
|
entity: b,
|
||||||
force: knockback
|
force: attack.knockback
|
||||||
* *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5),
|
* *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))
|
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)),
|
ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
use crate::{
|
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},
|
event::{EventBus, ServerEvent},
|
||||||
state::DeltaTime,
|
state::DeltaTime,
|
||||||
sync::Uid,
|
sync::{Uid, UidAllocator},
|
||||||
terrain::{Block, BlockKind, TerrainGrid},
|
terrain::{Block, BlockKind, TerrainGrid},
|
||||||
vol::ReadVol,
|
vol::ReadVol,
|
||||||
};
|
};
|
||||||
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
|
use specs::{
|
||||||
|
saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
|
||||||
|
};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
pub const GRAVITY: f32 = 9.81 * 5.0;
|
pub const GRAVITY: f32 = 9.81 * 5.0;
|
||||||
@ -44,6 +49,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadStorage<'a, Uid>,
|
ReadStorage<'a, Uid>,
|
||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, TerrainGrid>,
|
||||||
Read<'a, DeltaTime>,
|
Read<'a, DeltaTime>,
|
||||||
|
Read<'a, UidAllocator>,
|
||||||
Read<'a, EventBus<ServerEvent>>,
|
Read<'a, EventBus<ServerEvent>>,
|
||||||
ReadStorage<'a, Scale>,
|
ReadStorage<'a, Scale>,
|
||||||
ReadStorage<'a, Sticky>,
|
ReadStorage<'a, Sticky>,
|
||||||
@ -55,6 +61,8 @@ impl<'a> System<'a> for Sys {
|
|||||||
WriteStorage<'a, Vel>,
|
WriteStorage<'a, Vel>,
|
||||||
WriteStorage<'a, Ori>,
|
WriteStorage<'a, Ori>,
|
||||||
ReadStorage<'a, Mounting>,
|
ReadStorage<'a, Mounting>,
|
||||||
|
ReadStorage<'a, Group>,
|
||||||
|
ReadStorage<'a, Projectile>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
|
||||||
@ -66,6 +74,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
uids,
|
uids,
|
||||||
terrain,
|
terrain,
|
||||||
dt,
|
dt,
|
||||||
|
uid_allocator,
|
||||||
event_bus,
|
event_bus,
|
||||||
scales,
|
scales,
|
||||||
stickies,
|
stickies,
|
||||||
@ -77,6 +86,8 @@ impl<'a> System<'a> for Sys {
|
|||||||
mut velocities,
|
mut velocities,
|
||||||
mut orientations,
|
mut orientations,
|
||||||
mountings,
|
mountings,
|
||||||
|
groups,
|
||||||
|
projectiles,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
let mut event_emitter = event_bus.emitter();
|
let mut event_emitter = event_bus.emitter();
|
||||||
@ -432,7 +443,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply pushback
|
// Apply pushback
|
||||||
for (pos, scale, mass, vel, _, _, _, physics) in (
|
for (pos, scale, mass, vel, _, _, _, physics, projectile) in (
|
||||||
&positions,
|
&positions,
|
||||||
scales.maybe(),
|
scales.maybe(),
|
||||||
masses.maybe(),
|
masses.maybe(),
|
||||||
@ -441,9 +452,12 @@ impl<'a> System<'a> for Sys {
|
|||||||
!&mountings,
|
!&mountings,
|
||||||
stickies.maybe(),
|
stickies.maybe(),
|
||||||
&mut physics_states,
|
&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()
|
.join()
|
||||||
.filter(|(_, _, _, _, _, _, sticky, physics)| {
|
.filter(|(_, _, _, _, _, _, sticky, physics, _)| {
|
||||||
sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground)
|
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 scale = scale.map(|s| s.0).unwrap_or(1.0);
|
||||||
let mass = mass.map(|m| m.0).unwrap_or(scale);
|
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,
|
&uids,
|
||||||
&positions,
|
&positions,
|
||||||
scales.maybe(),
|
scales.maybe(),
|
||||||
masses.maybe(),
|
masses.maybe(),
|
||||||
&colliders,
|
&colliders,
|
||||||
!&mountings,
|
!&mountings,
|
||||||
|
groups.maybe(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
|
if ignore_group.is_some() && ignore_group == group {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let scale_other = scale_other.map(|s| s.0).unwrap_or(1.0);
|
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);
|
let mass_other = mass_other.map(|m| m.0).unwrap_or(scale_other);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comp::{
|
comp::{
|
||||||
projectile, Alignment, Damage, DamageSource, Energy, EnergySource, HealthChange,
|
projectile, Damage, DamageSource, Energy, EnergySource, HealthChange, HealthSource,
|
||||||
HealthSource, Loadout, Ori, PhysicsState, Pos, Projectile, Vel,
|
Loadout, Ori, PhysicsState, Pos, Projectile, Vel,
|
||||||
},
|
},
|
||||||
event::{EventBus, LocalEvent, ServerEvent},
|
event::{EventBus, LocalEvent, ServerEvent},
|
||||||
state::DeltaTime,
|
state::DeltaTime,
|
||||||
@ -28,7 +28,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
WriteStorage<'a, Ori>,
|
WriteStorage<'a, Ori>,
|
||||||
WriteStorage<'a, Projectile>,
|
WriteStorage<'a, Projectile>,
|
||||||
WriteStorage<'a, Energy>,
|
WriteStorage<'a, Energy>,
|
||||||
ReadStorage<'a, Alignment>,
|
|
||||||
ReadStorage<'a, Loadout>,
|
ReadStorage<'a, Loadout>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -46,7 +45,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
mut orientations,
|
mut orientations,
|
||||||
mut projectiles,
|
mut projectiles,
|
||||||
mut energies,
|
mut energies,
|
||||||
alignments,
|
|
||||||
loadouts,
|
loadouts,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
@ -72,6 +70,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
pos: pos.0,
|
pos: pos.0,
|
||||||
power,
|
power,
|
||||||
owner: projectile.owner,
|
owner: projectile.owner,
|
||||||
|
friendly_damage: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
|
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
|
||||||
@ -92,23 +91,13 @@ impl<'a> System<'a> for Sys {
|
|||||||
healthchange: healthchange as f32,
|
healthchange: healthchange as f32,
|
||||||
source: DamageSource::Projectile,
|
source: DamageSource::Projectile,
|
||||||
};
|
};
|
||||||
if let Some(entity) =
|
|
||||||
uid_allocator.retrieve_entity_internal(other.into())
|
let other_entity = uid_allocator.retrieve_entity_internal(other.into());
|
||||||
{
|
if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) {
|
||||||
if let Some(loadout) = loadouts.get(entity) {
|
damage.modify_damage(false, loadout);
|
||||||
damage.modify_damage(false, loadout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Hacky: remove this when groups get implemented
|
|
||||||
let passive = uid_allocator
|
if other != owner_uid {
|
||||||
.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 {
|
|
||||||
server_emitter.emit(ServerEvent::Damage {
|
server_emitter.emit(ServerEvent::Damage {
|
||||||
uid: other,
|
uid: other,
|
||||||
change: HealthChange {
|
change: HealthChange {
|
||||||
@ -143,6 +132,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
pos: pos.0,
|
pos: pos.0,
|
||||||
power,
|
power,
|
||||||
owner: projectile.owner,
|
owner: projectile.owner,
|
||||||
|
friendly_damage: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
|
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//! To implement a new command, add an instance of `ChatCommand` to
|
//! To implement a new command, add an instance of `ChatCommand` to
|
||||||
//! `CHAT_COMMANDS` and provide a handler function.
|
//! `CHAT_COMMANDS` and provide a handler function.
|
||||||
|
|
||||||
use crate::{Server, StateExt};
|
use crate::{client::Client, Server, StateExt};
|
||||||
use chrono::{NaiveTime, Timelike};
|
use chrono::{NaiveTime, Timelike};
|
||||||
use common::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
@ -77,7 +77,6 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
|
|||||||
ChatCommand::Health => handle_health,
|
ChatCommand::Health => handle_health,
|
||||||
ChatCommand::Help => handle_help,
|
ChatCommand::Help => handle_help,
|
||||||
ChatCommand::JoinFaction => handle_join_faction,
|
ChatCommand::JoinFaction => handle_join_faction,
|
||||||
ChatCommand::JoinGroup => handle_join_group,
|
|
||||||
ChatCommand::Jump => handle_jump,
|
ChatCommand::Jump => handle_jump,
|
||||||
ChatCommand::Kill => handle_kill,
|
ChatCommand::Kill => handle_kill,
|
||||||
ChatCommand::KillNpcs => handle_kill_npcs,
|
ChatCommand::KillNpcs => handle_kill_npcs,
|
||||||
@ -227,7 +226,7 @@ fn handle_jump(
|
|||||||
action: &ChatCommand,
|
action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
|
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) => {
|
Some(current_pos) => {
|
||||||
server
|
server
|
||||||
.state
|
.state
|
||||||
@ -252,7 +251,7 @@ fn handle_goto(
|
|||||||
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
|
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
|
||||||
if server
|
if server
|
||||||
.state
|
.state
|
||||||
.read_component_cloned::<comp::Pos>(target)
|
.read_component_copied::<comp::Pos>(target)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
server
|
server
|
||||||
@ -463,9 +462,9 @@ fn handle_tp(
|
|||||||
);
|
);
|
||||||
return;
|
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(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, pos);
|
||||||
server.state.write_component(target, comp::ForceUpdate);
|
server.state.write_component(target, comp::ForceUpdate);
|
||||||
} else {
|
} else {
|
||||||
@ -510,7 +509,7 @@ fn handle_spawn(
|
|||||||
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => {
|
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => {
|
||||||
let uid = server
|
let uid = server
|
||||||
.state
|
.state
|
||||||
.read_component_cloned(target)
|
.read_component_copied(target)
|
||||||
.expect("Expected player to have a UID");
|
.expect("Expected player to have a UID");
|
||||||
if let Some(alignment) = parse_alignment(uid, &opt_align) {
|
if let Some(alignment) = parse_alignment(uid, &opt_align) {
|
||||||
let amount = opt_amount
|
let amount = opt_amount
|
||||||
@ -521,7 +520,7 @@ fn handle_spawn(
|
|||||||
|
|
||||||
let ai = opt_ai.unwrap_or_else(|| "true".to_string());
|
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) => {
|
Some(pos) => {
|
||||||
let agent =
|
let agent =
|
||||||
if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment {
|
if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment {
|
||||||
@ -557,6 +556,43 @@ fn handle_spawn(
|
|||||||
|
|
||||||
let new_entity = entity_base.build();
|
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) {
|
if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) {
|
||||||
server.notify_client(
|
server.notify_client(
|
||||||
client,
|
client,
|
||||||
@ -594,7 +630,7 @@ fn handle_spawn_training_dummy(
|
|||||||
_args: String,
|
_args: String,
|
||||||
_action: &ChatCommand,
|
_action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
match server.state.read_component_cloned::<comp::Pos>(target) {
|
match server.state.read_component_copied::<comp::Pos>(target) {
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
let vel = Vec3::new(
|
let vel = Vec3::new(
|
||||||
rand::thread_rng().gen_range(-2.0, 3.0),
|
rand::thread_rng().gen_range(-2.0, 3.0),
|
||||||
@ -961,13 +997,14 @@ fn handle_explosion(
|
|||||||
|
|
||||||
let ecs = server.state.ecs();
|
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) => {
|
Some(pos) => {
|
||||||
ecs.read_resource::<EventBus<ServerEvent>>()
|
ecs.read_resource::<EventBus<ServerEvent>>()
|
||||||
.emit_now(ServerEvent::Explosion {
|
.emit_now(ServerEvent::Explosion {
|
||||||
pos: pos.0,
|
pos: pos.0,
|
||||||
power,
|
power,
|
||||||
owner: ecs.read_storage::<Uid>().get(target).copied(),
|
owner: ecs.read_storage::<Uid>().get(target).copied(),
|
||||||
|
friendly_damage: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
None => server.notify_client(
|
None => server.notify_client(
|
||||||
@ -984,7 +1021,7 @@ fn handle_waypoint(
|
|||||||
_args: String,
|
_args: String,
|
||||||
_action: &ChatCommand,
|
_action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
match server.state.read_component_cloned::<comp::Pos>(target) {
|
match server.state.read_component_copied::<comp::Pos>(target) {
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
let time = server.state.ecs().read_resource();
|
let time = server.state.ecs().read_resource();
|
||||||
let _ = server
|
let _ = server
|
||||||
@ -1020,7 +1057,7 @@ fn handle_adminify(
|
|||||||
Some(player) => {
|
Some(player) => {
|
||||||
let is_admin = if server
|
let is_admin = if server
|
||||||
.state
|
.state
|
||||||
.read_component_cloned::<comp::Admin>(player)
|
.read_component_copied::<comp::Admin>(player)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
ecs.write_storage::<comp::Admin>().remove(player);
|
ecs.write_storage::<comp::Admin>().remove(player);
|
||||||
@ -1161,8 +1198,8 @@ fn handle_group(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ecs = server.state.ecs();
|
let ecs = server.state.ecs();
|
||||||
if let Some(comp::Group(group)) = ecs.read_storage().get(client) {
|
if let Some(group) = ecs.read_storage::<comp::Group>().get(client) {
|
||||||
let mode = comp::ChatMode::Group(group.to_string());
|
let mode = comp::ChatMode::Group(*group);
|
||||||
let _ = ecs.write_storage().insert(client, mode.clone());
|
let _ = ecs.write_storage().insert(client, mode.clone());
|
||||||
if !msg.is_empty() {
|
if !msg.is_empty() {
|
||||||
if let Some(uid) = ecs.read_storage().get(client) {
|
if let Some(uid) = ecs.read_storage().get(client) {
|
||||||
@ -1172,7 +1209,7 @@ fn handle_group(
|
|||||||
} else {
|
} else {
|
||||||
server.notify_client(
|
server.notify_client(
|
||||||
client,
|
client,
|
||||||
ChatType::CommandError.server_msg("Please join a group with /join_group"),
|
ChatType::CommandError.server_msg("Please create a group first"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1323,68 +1360,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"))]
|
#[cfg(not(feature = "worldgen"))]
|
||||||
fn handle_debug_column(
|
fn handle_debug_column(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
@ -1626,7 +1601,7 @@ fn handle_remove_lights(
|
|||||||
action: &ChatCommand,
|
action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32);
|
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![];
|
let mut to_delete = vec![];
|
||||||
|
|
||||||
match opt_player_pos {
|
match opt_player_pos {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{sys, Server, StateExt};
|
use crate::{sys, Server, StateExt};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos,
|
self, group, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos,
|
||||||
Projectile, Scale, Stats, Vel, WaypointArea,
|
Projectile, Scale, Stats, Vel, WaypointArea,
|
||||||
},
|
},
|
||||||
util::Dir,
|
util::Dir,
|
||||||
@ -36,12 +36,26 @@ pub fn handle_create_npc(
|
|||||||
scale: Scale,
|
scale: Scale,
|
||||||
drop_item: Option<Item>,
|
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
|
let entity = server
|
||||||
.state
|
.state
|
||||||
.create_npc(pos, stats, loadout, body)
|
.create_npc(pos, stats, loadout, body)
|
||||||
.with(scale)
|
.with(scale)
|
||||||
.with(alignment);
|
.with(alignment);
|
||||||
|
|
||||||
|
let entity = if let Some(group) = group {
|
||||||
|
entity.with(group)
|
||||||
|
} else {
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
|
||||||
let entity = if let Some(agent) = agent.into() {
|
let entity = if let Some(agent) = agent.into() {
|
||||||
entity.with(agent)
|
entity.with(agent)
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,17 +2,17 @@ use crate::{client::Client, Server, SpawnPoint, StateExt};
|
|||||||
use common::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
comp::{
|
comp::{
|
||||||
self, item::lottery::Lottery, object, Body, Damage, DamageSource, HealthChange,
|
self, item::lottery::Lottery, object, Alignment, Body, Damage, DamageSource, Group,
|
||||||
HealthSource, Player, Stats,
|
HealthChange, HealthSource, Player, Pos, Stats,
|
||||||
},
|
},
|
||||||
msg::{PlayerListUpdate, ServerMsg},
|
msg::{PlayerListUpdate, ServerMsg},
|
||||||
state::BlockChange,
|
state::BlockChange,
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, UidAllocator, WorldSyncExt},
|
||||||
sys::combat::BLOCK_ANGLE,
|
sys::combat::BLOCK_ANGLE,
|
||||||
terrain::{Block, TerrainGrid},
|
terrain::{Block, TerrainGrid},
|
||||||
vol::{ReadVol, Vox},
|
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 tracing::error;
|
||||||
use vek::Vec3;
|
use vek::Vec3;
|
||||||
|
|
||||||
@ -55,28 +55,88 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
|
|||||||
state.notify_registered_clients(comp::ChatType::Kill.server_msg(msg));
|
state.notify_registered_clients(comp::ChatType::Kill.server_msg(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Give EXP to the killer if entity had stats
|
||||||
// Give EXP to the killer if entity had stats
|
(|| {
|
||||||
let mut stats = state.ecs().write_storage::<Stats>();
|
let mut stats = state.ecs().write_storage::<Stats>();
|
||||||
if let Some(entity_stats) = stats.get(entity).cloned() {
|
let by = if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } =
|
||||||
if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } =
|
cause
|
||||||
cause
|
{
|
||||||
{
|
by
|
||||||
state.ecs().entity_from_uid(by.into()).map(|attacker| {
|
} else {
|
||||||
if let Some(attacker_stats) = stats.get_mut(attacker) {
|
return;
|
||||||
// TODO: Discuss whether we should give EXP by Player
|
};
|
||||||
// Killing or not.
|
let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) {
|
||||||
attacker_stats.exp.change_by(
|
attacker
|
||||||
(entity_stats.body_type.base_exp()
|
} else {
|
||||||
+ entity_stats.level.level()
|
return;
|
||||||
* entity_stats.body_type.base_exp_increase())
|
};
|
||||||
as i64,
|
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
|
if state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -189,7 +249,7 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
|
|||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
let respawn_point = state
|
let respawn_point = state
|
||||||
.read_component_cloned::<comp::Waypoint>(entity)
|
.read_component_copied::<comp::Waypoint>(entity)
|
||||||
.map(|wp| wp.get_pos())
|
.map(|wp| wp.get_pos())
|
||||||
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);
|
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);
|
||||||
|
|
||||||
@ -217,11 +277,25 @@ 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
|
// Go through all other entities
|
||||||
let hit_range = 3.0 * power;
|
let hit_range = 3.0 * power;
|
||||||
let ecs = &server.state.ecs();
|
let ecs = &server.state.ecs();
|
||||||
for (pos_b, ori_b, character_b, stats_b, loadout_b) in (
|
|
||||||
|
let owner_entity = owner.and_then(|uid| {
|
||||||
|
ecs.read_resource::<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::Pos>(),
|
||||||
&ecs.read_storage::<comp::Ori>(),
|
&ecs.read_storage::<comp::Ori>(),
|
||||||
ecs.read_storage::<comp::CharacterState>().maybe(),
|
ecs.read_storage::<comp::CharacterState>().maybe(),
|
||||||
@ -233,9 +307,13 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, power: f32, owner: Opti
|
|||||||
let distance_squared = pos.distance_squared(pos_b.0);
|
let distance_squared = pos.distance_squared(pos_b.0);
|
||||||
// Check if it is a hit
|
// Check if it is a hit
|
||||||
if !stats_b.is_dead
|
if !stats_b.is_dead
|
||||||
// Spherical wedge shaped attack field
|
|
||||||
// RADIUS
|
// RADIUS
|
||||||
&& distance_squared < hit_range.powi(2)
|
&& 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
|
// Weapon gives base damage
|
||||||
let dmg = (1.0 - distance_squared / hit_range.powi(2)) * power * 130.0;
|
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::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
self, item,
|
self, item,
|
||||||
slot::{self, Slot},
|
slot::{self, Slot},
|
||||||
Pos, MAX_PICKUP_RANGE_SQR,
|
Pos, MAX_PICKUP_RANGE_SQR,
|
||||||
},
|
},
|
||||||
|
msg::ServerMsg,
|
||||||
recipe::default_recipe_book,
|
recipe::default_recipe_book,
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, WorldSyncExt},
|
||||||
terrain::block::Block,
|
terrain::block::Block,
|
||||||
@ -166,10 +167,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
thrown_items.push((
|
thrown_items.push((
|
||||||
*pos,
|
*pos,
|
||||||
state
|
state
|
||||||
.read_component_cloned::<comp::Vel>(entity)
|
.read_component_copied::<comp::Vel>(entity)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
state
|
state
|
||||||
.read_component_cloned::<comp::Ori>(entity)
|
.read_component_copied::<comp::Ori>(entity)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
*kind,
|
*kind,
|
||||||
));
|
));
|
||||||
@ -184,7 +185,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
state.read_storage::<comp::Pos>().get(entity)
|
state.read_storage::<comp::Pos>().get(entity)
|
||||||
{
|
{
|
||||||
let uid = state
|
let uid = state
|
||||||
.read_component_cloned(entity)
|
.read_component_copied(entity)
|
||||||
.expect("Expected player to have a UID");
|
.expect("Expected player to have a UID");
|
||||||
if (
|
if (
|
||||||
&state.read_storage::<comp::Alignment>(),
|
&state.read_storage::<comp::Alignment>(),
|
||||||
@ -222,6 +223,35 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
.ecs()
|
.ecs()
|
||||||
.write_storage()
|
.write_storage()
|
||||||
.insert(tameable_entity, comp::Alignment::Owned(uid));
|
.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
|
let _ = state
|
||||||
.ecs()
|
.ecs()
|
||||||
.write_storage()
|
.write_storage()
|
||||||
@ -311,7 +341,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
dropped_items.push((
|
dropped_items.push((
|
||||||
*pos,
|
*pos,
|
||||||
state
|
state
|
||||||
.read_component_cloned::<comp::Ori>(entity)
|
.read_component_copied::<comp::Ori>(entity)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
item,
|
item,
|
||||||
));
|
));
|
||||||
@ -343,10 +373,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
for _ in 0..amount {
|
for _ in 0..amount {
|
||||||
dropped_items.push((
|
dropped_items.push((
|
||||||
state
|
state
|
||||||
.read_component_cloned::<comp::Pos>(entity)
|
.read_component_copied::<comp::Pos>(entity)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
state
|
state
|
||||||
.read_component_cloned::<comp::Ori>(entity)
|
.read_component_copied::<comp::Ori>(entity)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
item.clone(),
|
item.clone(),
|
||||||
));
|
));
|
||||||
@ -377,7 +407,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
+ Vec3::unit_z() * 15.0
|
+ Vec3::unit_z() * 15.0
|
||||||
+ Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.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
|
let mut new_entity = state
|
||||||
.create_object(Default::default(), match kind {
|
.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_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up,
|
||||||
handle_respawn,
|
handle_respawn,
|
||||||
};
|
};
|
||||||
|
use group_manip::handle_group;
|
||||||
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
|
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
|
||||||
use inventory_manip::handle_inventory;
|
use inventory_manip::handle_inventory;
|
||||||
use player::{handle_client_disconnect, handle_exit_ingame};
|
use player::{handle_client_disconnect, handle_exit_ingame};
|
||||||
@ -15,6 +16,7 @@ use specs::{Entity as EcsEntity, WorldExt};
|
|||||||
|
|
||||||
mod entity_creation;
|
mod entity_creation;
|
||||||
mod entity_manipulation;
|
mod entity_manipulation;
|
||||||
|
mod group_manip;
|
||||||
mod interaction;
|
mod interaction;
|
||||||
mod inventory_manip;
|
mod inventory_manip;
|
||||||
mod player;
|
mod player;
|
||||||
@ -48,9 +50,12 @@ impl Server {
|
|||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
ServerEvent::Explosion { pos, power, owner } => {
|
ServerEvent::Explosion {
|
||||||
handle_explosion(&self, pos, power, owner)
|
pos,
|
||||||
},
|
power,
|
||||||
|
owner,
|
||||||
|
friendly_damage,
|
||||||
|
} => handle_explosion(&self, pos, power, owner, friendly_damage),
|
||||||
ServerEvent::Shoot {
|
ServerEvent::Shoot {
|
||||||
entity,
|
entity,
|
||||||
dir,
|
dir,
|
||||||
@ -62,6 +67,7 @@ impl Server {
|
|||||||
ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change),
|
ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change),
|
||||||
ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause),
|
ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause),
|
||||||
ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip),
|
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::Respawn(entity) => handle_respawn(&self, entity),
|
||||||
ServerEvent::LandOnGround { entity, vel } => {
|
ServerEvent::LandOnGround { entity, vel } => {
|
||||||
handle_land_on_ground(&self, entity, vel)
|
handle_land_on_ground(&self, entity, vel)
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp,
|
||||||
comp::Player,
|
comp::{group, Player},
|
||||||
msg::{ClientState, PlayerListUpdate, ServerMsg},
|
msg::{ClientState, PlayerListUpdate, ServerMsg},
|
||||||
sync::{Uid, UidAllocator},
|
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
|
// Note: If other `ServerEvent`s are referring to this entity they will be
|
||||||
// disrupted
|
// disrupted
|
||||||
let maybe_client = state.ecs().write_storage::<Client>().remove(entity);
|
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_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) {
|
if let (Some(mut client), Some(uid), Some(player)) = (maybe_client, maybe_uid, maybe_player) {
|
||||||
// Tell client its request was successful
|
// Tell client its request was successful
|
||||||
client.allow_state(ClientState::Registered);
|
client.allow_state(ClientState::Registered);
|
||||||
@ -29,13 +34,39 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
|
|||||||
client.notify(ServerMsg::ExitIngameCleanup);
|
client.notify(ServerMsg::ExitIngameCleanup);
|
||||||
|
|
||||||
let entity_builder = state.ecs_mut().create_entity().with(client).with(player);
|
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
|
// Ensure UidAllocator maps this uid to the new entity
|
||||||
let uid = entity_builder
|
let uid = entity_builder
|
||||||
.world
|
.world
|
||||||
.write_resource::<UidAllocator>()
|
.write_resource::<UidAllocator>()
|
||||||
.allocate(entity_builder.entity, Some(uid.into()));
|
.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
|
// Delete old entity
|
||||||
if let Err(e) = state.delete_entity_recorded(entity) {
|
if let Err(e) = state.delete_entity_recorded(entity) {
|
||||||
error!(
|
error!(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![allow(clippy::option_map_unit_fn)]
|
#![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 alias_validator;
|
||||||
pub mod chunk_generator;
|
pub mod chunk_generator;
|
||||||
@ -127,6 +127,7 @@ impl Server {
|
|||||||
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
|
||||||
state.ecs_mut().insert(sys::TerrainTimer::default());
|
state.ecs_mut().insert(sys::TerrainTimer::default());
|
||||||
state.ecs_mut().insert(sys::WaypointTimer::default());
|
state.ecs_mut().insert(sys::WaypointTimer::default());
|
||||||
|
state.ecs_mut().insert(sys::InviteTimeoutTimer::default());
|
||||||
state.ecs_mut().insert(sys::PersistenceTimer::default());
|
state.ecs_mut().insert(sys::PersistenceTimer::default());
|
||||||
|
|
||||||
// System schedulers to control execution of systems
|
// System schedulers to control execution of systems
|
||||||
@ -508,12 +509,18 @@ impl Server {
|
|||||||
.nanos as i64;
|
.nanos as i64;
|
||||||
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().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 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
|
let stats_persistence_nanos = self
|
||||||
.state
|
.state
|
||||||
.ecs()
|
.ecs()
|
||||||
.read_resource::<sys::PersistenceTimer>()
|
.read_resource::<sys::PersistenceTimer>()
|
||||||
.nanos as i64;
|
.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
|
// Report timing info
|
||||||
self.tick_metrics
|
self.tick_metrics
|
||||||
@ -575,6 +582,10 @@ impl Server {
|
|||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["waypoint"])
|
.with_label_values(&["waypoint"])
|
||||||
.set(waypoint_nanos);
|
.set(waypoint_nanos);
|
||||||
|
self.tick_metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["invite timeout"])
|
||||||
|
.set(invite_timeout_nanos);
|
||||||
self.tick_metrics
|
self.tick_metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["persistence:stats"])
|
.with_label_values(&["persistence:stats"])
|
||||||
@ -684,6 +695,7 @@ impl Server {
|
|||||||
.create_entity_package(entity, None, None, None),
|
.create_entity_package(entity, None, None, None),
|
||||||
server_info: self.get_server_info(),
|
server_info: self.get_server_info(),
|
||||||
time_of_day: *self.state.ecs().read_resource(),
|
time_of_day: *self.state.ecs().read_resource(),
|
||||||
|
max_group_size: self.settings().max_player_group_size,
|
||||||
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
|
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
|
||||||
recipe_book: (&*default_recipe_book()).clone(),
|
recipe_book: (&*default_recipe_book()).clone(),
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ pub struct ServerSettings {
|
|||||||
pub persistence_db_dir: String,
|
pub persistence_db_dir: String,
|
||||||
pub max_view_distance: Option<u32>,
|
pub max_view_distance: Option<u32>,
|
||||||
pub banned_words_files: Vec<PathBuf>,
|
pub banned_words_files: Vec<PathBuf>,
|
||||||
|
pub max_player_group_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerSettings {
|
impl Default for ServerSettings {
|
||||||
@ -65,6 +66,7 @@ impl Default for ServerSettings {
|
|||||||
persistence_db_dir: "saves".to_owned(),
|
persistence_db_dir: "saves".to_owned(),
|
||||||
max_view_distance: Some(30),
|
max_view_distance: Some(30),
|
||||||
banned_words_files: Vec::new(),
|
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
|
/// Performed after loading component data from the database
|
||||||
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
|
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
|
||||||
/// Iterates over registered clients and send each `ServerMsg`
|
/// 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);
|
fn notify_registered_clients(&self, msg: ServerMsg);
|
||||||
/// Delete an entity, recording the deletion in [`DeletedEntities`]
|
/// Delete an entity, recording the deletion in [`DeletedEntities`]
|
||||||
fn delete_entity_recorded(
|
fn delete_entity_recorded(
|
||||||
@ -173,7 +173,7 @@ impl StateExt for State {
|
|||||||
self.write_component(entity, comp::CharacterState::default());
|
self.write_component(entity, comp::CharacterState::default());
|
||||||
self.write_component(
|
self.write_component(
|
||||||
entity,
|
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
|
// Set the character id for the player
|
||||||
@ -213,7 +213,7 @@ impl StateExt for State {
|
|||||||
|
|
||||||
// Notify clients of a player list update
|
// Notify clients of a player list update
|
||||||
let client_uid = self
|
let client_uid = self
|
||||||
.read_component_cloned::<Uid>(entity)
|
.read_component_copied::<Uid>(entity)
|
||||||
.map(|u| u)
|
.map(|u| u)
|
||||||
.expect("Client doesn't have a Uid!!!");
|
.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
|
/// Send the chat message to the proper players. Say and region are limited
|
||||||
/// by location. Faction and group are limited by component.
|
/// 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 ecs = self.ecs();
|
||||||
let is_within =
|
let is_within =
|
||||||
|target, a: &comp::Pos, b: &comp::Pos| a.0.distance_squared(b.0) < target * target;
|
|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 {
|
match &msg.chat_type {
|
||||||
comp::ChatType::Online
|
comp::ChatType::Online
|
||||||
| comp::ChatType::Offline
|
| comp::ChatType::Offline
|
||||||
@ -253,7 +261,7 @@ impl StateExt for State {
|
|||||||
| comp::ChatType::Kill
|
| comp::ChatType::Kill
|
||||||
| comp::ChatType::Meta
|
| comp::ChatType::Meta
|
||||||
| comp::ChatType::World(_) => {
|
| comp::ChatType::World(_) => {
|
||||||
self.notify_registered_clients(ServerMsg::ChatMsg(msg.clone()))
|
self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg))
|
||||||
},
|
},
|
||||||
comp::ChatType::Tell(u, t) => {
|
comp::ChatType::Tell(u, t) => {
|
||||||
for (client, uid) in (
|
for (client, uid) in (
|
||||||
@ -263,7 +271,7 @@ impl StateExt for State {
|
|||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
if uid == u || uid == t {
|
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)) {
|
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||||
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
||||||
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
|
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)) {
|
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||||
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
||||||
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
|
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)) {
|
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
|
||||||
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
|
||||||
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
|
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()
|
.join()
|
||||||
{
|
{
|
||||||
if s == &faction.0 {
|
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 (
|
for (client, group) in (
|
||||||
&mut ecs.write_storage::<Client>(),
|
&mut ecs.write_storage::<Client>(),
|
||||||
&ecs.read_storage::<comp::Group>(),
|
&ecs.read_storage::<comp::Group>(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
if s == &group.0 {
|
if g == group {
|
||||||
client.notify(ServerMsg::ChatMsg(msg.clone()));
|
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -346,6 +354,30 @@ impl StateExt for State {
|
|||||||
&mut self,
|
&mut self,
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
) -> Result<(), specs::error::WrongGeneration> {
|
) -> 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) = (
|
let (maybe_uid, maybe_pos) = (
|
||||||
self.ecs().read_storage::<Uid>().get(entity).copied(),
|
self.ecs().read_storage::<Uid>().get(entity).copied(),
|
||||||
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),
|
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),
|
||||||
|
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::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
Admin, AdminList, CanBuild, ChatMode, ChatMsg, ChatType, ControlEvent, Controller,
|
Admin, AdminList, CanBuild, ChatMode, ChatType, ControlEvent, Controller, ForceUpdate, Ori,
|
||||||
ForceUpdate, Ori, Player, Pos, Stats, Vel,
|
Player, Pos, Stats, UnresolvedChatMsg, Vel,
|
||||||
},
|
},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{
|
msg::{
|
||||||
@ -32,7 +32,7 @@ impl Sys {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn handle_client_msg(
|
async fn handle_client_msg(
|
||||||
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
|
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>,
|
player_list: &HashMap<Uid, PlayerInfo>,
|
||||||
new_players: &mut Vec<specs::Entity>,
|
new_players: &mut Vec<specs::Entity>,
|
||||||
entity: specs::Entity,
|
entity: specs::Entity,
|
||||||
@ -202,7 +202,7 @@ impl Sys {
|
|||||||
// Only send login message if it wasn't already
|
// Only send login message if it wasn't already
|
||||||
// sent previously
|
// sent previously
|
||||||
if !client.login_msg_sent {
|
if !client.login_msg_sent {
|
||||||
new_chat_msgs.push((None, ChatMsg {
|
new_chat_msgs.push((None, UnresolvedChatMsg {
|
||||||
chat_type: ChatType::Online,
|
chat_type: ChatType::Online,
|
||||||
message: format!("[{}] is now online.", &player.alias), // TODO: Localize this
|
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 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.
|
// Player list to send new players.
|
||||||
let player_list = (&uids, &players, stats.maybe(), admins.maybe())
|
let player_list = (&uids, &players, stats.maybe(), admins.maybe())
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
pub mod entity_sync;
|
pub mod entity_sync;
|
||||||
|
pub mod invite_timeout;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
@ -21,6 +22,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
|
|||||||
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
pub type TerrainTimer = SysTimer<terrain::Sys>;
|
||||||
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
|
||||||
pub type WaypointTimer = SysTimer<waypoint::Sys>;
|
pub type WaypointTimer = SysTimer<waypoint::Sys>;
|
||||||
|
pub type InviteTimeoutTimer = SysTimer<invite_timeout::Sys>;
|
||||||
pub type PersistenceTimer = SysTimer<persistence::Sys>;
|
pub type PersistenceTimer = SysTimer<persistence::Sys>;
|
||||||
pub type PersistenceScheduler = SysScheduler<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_SYNC_SYS: &str = "server_terrain_sync_sys";
|
||||||
const TERRAIN_SYS: &str = "server_terrain_sys";
|
const TERRAIN_SYS: &str = "server_terrain_sys";
|
||||||
const WAYPOINT_SYS: &str = "server_waypoint_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 PERSISTENCE_SYS: &str = "server_persistence_sys";
|
||||||
const OBJECT_SYS: &str = "server_object_sys";
|
const OBJECT_SYS: &str = "server_object_sys";
|
||||||
|
|
||||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||||
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
|
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
|
||||||
dispatch_builder.add(waypoint::Sys, WAYPOINT_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(persistence::Sys, PERSISTENCE_SYS, &[]);
|
||||||
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
|
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
pos: pos.0,
|
pos: pos.0,
|
||||||
power: 4.0,
|
power: 4.0,
|
||||||
owner: *owner,
|
owner: *owner,
|
||||||
|
friendly_damage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use super::SysTimer;
|
use super::SysTimer;
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
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,
|
Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
|
||||||
},
|
},
|
||||||
msg::EcsCompPacket,
|
msg::EcsCompPacket,
|
||||||
@ -48,7 +48,7 @@ pub struct TrackedComps<'a> {
|
|||||||
pub scale: ReadStorage<'a, Scale>,
|
pub scale: ReadStorage<'a, Scale>,
|
||||||
pub mounting: ReadStorage<'a, Mounting>,
|
pub mounting: ReadStorage<'a, Mounting>,
|
||||||
pub mount_state: ReadStorage<'a, MountState>,
|
pub mount_state: ReadStorage<'a, MountState>,
|
||||||
pub alignment: ReadStorage<'a, Alignment>,
|
pub group: ReadStorage<'a, Group>,
|
||||||
pub mass: ReadStorage<'a, Mass>,
|
pub mass: ReadStorage<'a, Mass>,
|
||||||
pub collider: ReadStorage<'a, Collider>,
|
pub collider: ReadStorage<'a, Collider>,
|
||||||
pub sticky: ReadStorage<'a, Sticky>,
|
pub sticky: ReadStorage<'a, Sticky>,
|
||||||
@ -105,7 +105,7 @@ impl<'a> TrackedComps<'a> {
|
|||||||
.get(entity)
|
.get(entity)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|c| comps.push(c.into()));
|
.map(|c| comps.push(c.into()));
|
||||||
self.alignment
|
self.group
|
||||||
.get(entity)
|
.get(entity)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|c| comps.push(c.into()));
|
.map(|c| comps.push(c.into()));
|
||||||
@ -151,7 +151,7 @@ pub struct ReadTrackers<'a> {
|
|||||||
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
|
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
|
||||||
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
|
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
|
||||||
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
|
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 mass: ReadExpect<'a, UpdateTracker<Mass>>,
|
||||||
pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
|
pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
|
||||||
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
|
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.scale, &comps.scale, filter)
|
||||||
.with_component(&comps.uid, &*self.mounting, &comps.mounting, 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.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.mass, &comps.mass, filter)
|
||||||
.with_component(&comps.uid, &*self.collider, &comps.collider, filter)
|
.with_component(&comps.uid, &*self.collider, &comps.collider, filter)
|
||||||
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
|
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
|
||||||
@ -214,7 +214,7 @@ pub struct WriteTrackers<'a> {
|
|||||||
scale: WriteExpect<'a, UpdateTracker<Scale>>,
|
scale: WriteExpect<'a, UpdateTracker<Scale>>,
|
||||||
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
|
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
|
||||||
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
|
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
|
||||||
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
|
group: WriteExpect<'a, UpdateTracker<Group>>,
|
||||||
mass: WriteExpect<'a, UpdateTracker<Mass>>,
|
mass: WriteExpect<'a, UpdateTracker<Mass>>,
|
||||||
collider: WriteExpect<'a, UpdateTracker<Collider>>,
|
collider: WriteExpect<'a, UpdateTracker<Collider>>,
|
||||||
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
|
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
|
||||||
@ -236,7 +236,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
|
|||||||
trackers.scale.record_changes(&comps.scale);
|
trackers.scale.record_changes(&comps.scale);
|
||||||
trackers.mounting.record_changes(&comps.mounting);
|
trackers.mounting.record_changes(&comps.mounting);
|
||||||
trackers.mount_state.record_changes(&comps.mount_state);
|
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.mass.record_changes(&comps.mass);
|
||||||
trackers.collider.record_changes(&comps.collider);
|
trackers.collider.record_changes(&comps.collider);
|
||||||
trackers.sticky.record_changes(&comps.sticky);
|
trackers.sticky.record_changes(&comps.sticky);
|
||||||
@ -291,7 +291,7 @@ pub fn register_trackers(world: &mut World) {
|
|||||||
world.register_tracker::<Scale>();
|
world.register_tracker::<Scale>();
|
||||||
world.register_tracker::<Mounting>();
|
world.register_tracker::<Mounting>();
|
||||||
world.register_tracker::<MountState>();
|
world.register_tracker::<MountState>();
|
||||||
world.register_tracker::<Alignment>();
|
world.register_tracker::<Group>();
|
||||||
world.register_tracker::<Mass>();
|
world.register_tracker::<Mass>();
|
||||||
world.register_tracker::<Collider>();
|
world.register_tracker::<Collider>();
|
||||||
world.register_tracker::<Sticky>();
|
world.register_tracker::<Sticky>();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use common::{
|
use common::{
|
||||||
generation::{ChunkSupplement, EntityInfo, EntityKind},
|
generation::{ChunkSupplement, EntityInfo},
|
||||||
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
||||||
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
||||||
};
|
};
|
||||||
@ -30,17 +30,10 @@ impl World {
|
|||||||
|
|
||||||
let mut supplement = ChunkSupplement::default();
|
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((
|
Ok((
|
||||||
TerrainChunk::new(
|
TerrainChunk::new(
|
||||||
256 + if rng.gen::<u8>() < 64 { height } else { 0 },
|
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(),
|
Block::empty(),
|
||||||
TerrainChunkMeta::void(),
|
TerrainChunkMeta::void(),
|
||||||
),
|
),
|
||||||
|
@ -229,7 +229,7 @@ impl<'a> Widget for Bag<'a> {
|
|||||||
)
|
)
|
||||||
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
|
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(22))
|
.font_size(self.fonts.cyri.scale(20))
|
||||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||||
.set(state.ids.inventory_title_bg, ui);
|
.set(state.ids.inventory_title_bg, ui);
|
||||||
Text::new(
|
Text::new(
|
||||||
@ -240,7 +240,7 @@ impl<'a> Widget for Bag<'a> {
|
|||||||
)
|
)
|
||||||
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
|
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(22))
|
.font_size(self.fonts.cyri.scale(20))
|
||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(state.ids.inventory_title, ui);
|
.set(state.ids.inventory_title, ui);
|
||||||
// Scrollbar-BG
|
// Scrollbar-BG
|
||||||
@ -585,7 +585,7 @@ impl<'a> Widget for Bag<'a> {
|
|||||||
"{}\n\n{}\n\n{}\n\n{}%",
|
"{}\n\n{}\n\n{}\n\n{}%",
|
||||||
self.stats.endurance, self.stats.fitness, self.stats.willpower, damage_reduction
|
self.stats.endurance, self.stats.fitness, self.stats.willpower, damage_reduction
|
||||||
))
|
))
|
||||||
.top_right_with_margins_on(state.ids.stats_alignment, 120.0, 150.0)
|
.top_right_with_margins_on(state.ids.stats_alignment, 120.0, 130.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(16))
|
.font_size(self.fonts.cyri.scale(16))
|
||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
|
@ -41,7 +41,7 @@ widget_ids! {
|
|||||||
crafting_button_bg,
|
crafting_button_bg,
|
||||||
crafting_text,
|
crafting_text,
|
||||||
crafting_text_bg,
|
crafting_text_bg,
|
||||||
|
group_button,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(WidgetCommon)]
|
#[derive(WidgetCommon)]
|
||||||
@ -360,6 +360,7 @@ impl<'a> Widget for Buttons<'a> {
|
|||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(state.ids.spellbook_text, ui);
|
.set(state.ids.spellbook_text, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crafting
|
// Crafting
|
||||||
if Button::image(self.imgs.crafting_icon)
|
if Button::image(self.imgs.crafting_icon)
|
||||||
.w_h(25.0, 25.0)
|
.w_h(25.0, 25.0)
|
||||||
@ -396,6 +397,7 @@ impl<'a> Widget for Buttons<'a> {
|
|||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(state.ids.crafting_text, ui);
|
.set(state.ids.crafting_text, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -475,7 +475,7 @@ fn cursor_offset_to_index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the color and icon for the current line in the chat box
|
/// Get the color and icon for the current line in the chat box
|
||||||
fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
|
fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
|
||||||
match chat_type {
|
match chat_type {
|
||||||
ChatType::Online => (ONLINE_COLOR, imgs.chat_online_small),
|
ChatType::Online => (ONLINE_COLOR, imgs.chat_online_small),
|
||||||
ChatType::Offline => (OFFLINE_COLOR, imgs.chat_offline_small),
|
ChatType::Offline => (OFFLINE_COLOR, imgs.chat_offline_small),
|
||||||
|
@ -117,23 +117,6 @@ impl<'a> Widget for Crafting<'a> {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/*if state.ids.recipe_img_frame.len() < self.client.recipe_book().iter().len() {
|
|
||||||
state.update(|state| {
|
|
||||||
state.ids.recipe_img_frame.resize(
|
|
||||||
self.client.recipe_book().iter().len(),
|
|
||||||
&mut ui.widget_id_generator(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if state.ids.recipe_img.len() < self.client.recipe_book().iter().len() {
|
|
||||||
state.update(|state| {
|
|
||||||
state.ids.recipe_img.resize(
|
|
||||||
self.client.recipe_book().iter().len(),
|
|
||||||
&mut ui.widget_id_generator(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
let ids = &state.ids;
|
let ids = &state.ids;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@ -186,7 +169,7 @@ impl<'a> Widget for Crafting<'a> {
|
|||||||
Text::new(&self.localized_strings.get("hud.crafting"))
|
Text::new(&self.localized_strings.get("hud.crafting"))
|
||||||
.mid_top_with_margin_on(ids.window_frame, 9.0)
|
.mid_top_with_margin_on(ids.window_frame, 9.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(22))
|
.font_size(self.fonts.cyri.scale(20))
|
||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(ids.title_main, ui);
|
.set(ids.title_main, ui);
|
||||||
|
|
||||||
|
685
voxygen/src/hud/group.rs
Normal file
685
voxygen/src/hud/group.rs
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
use super::{
|
||||||
|
img_ids::Imgs, Show, BLACK, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR,
|
||||||
|
MANA_COLOR, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
i18n::VoxygenLocalization, settings::Settings, ui::fonts::ConrodVoxygenFonts,
|
||||||
|
window::GameInput, GlobalState,
|
||||||
|
};
|
||||||
|
use client::{self, Client};
|
||||||
|
use common::{
|
||||||
|
comp::{group::Role, Stats},
|
||||||
|
sync::{Uid, WorldSyncExt},
|
||||||
|
};
|
||||||
|
use conrod_core::{
|
||||||
|
color,
|
||||||
|
position::{Place, Relative},
|
||||||
|
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
||||||
|
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||||
|
};
|
||||||
|
use specs::{saveload::MarkerAllocator, WorldExt};
|
||||||
|
|
||||||
|
widget_ids! {
|
||||||
|
pub struct Ids {
|
||||||
|
group_button,
|
||||||
|
bg,
|
||||||
|
title,
|
||||||
|
title_bg,
|
||||||
|
btn_bg,
|
||||||
|
btn_friend,
|
||||||
|
btn_leader,
|
||||||
|
btn_link,
|
||||||
|
btn_kick,
|
||||||
|
btn_leave,
|
||||||
|
scroll_area,
|
||||||
|
scrollbar,
|
||||||
|
members[],
|
||||||
|
bubble_frame,
|
||||||
|
btn_accept,
|
||||||
|
btn_decline,
|
||||||
|
member_panels_bg[],
|
||||||
|
member_panels_frame[],
|
||||||
|
member_panels_txt_bg[],
|
||||||
|
member_panels_txt[],
|
||||||
|
member_health[],
|
||||||
|
member_stam[],
|
||||||
|
dead_txt[],
|
||||||
|
health_txt[],
|
||||||
|
timeout_bg,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
ids: Ids,
|
||||||
|
// Selected group member
|
||||||
|
selected_member: Option<Uid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(WidgetCommon)]
|
||||||
|
pub struct Group<'a> {
|
||||||
|
show: &'a mut Show,
|
||||||
|
client: &'a Client,
|
||||||
|
settings: &'a Settings,
|
||||||
|
imgs: &'a Imgs,
|
||||||
|
fonts: &'a ConrodVoxygenFonts,
|
||||||
|
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
|
pulse: f32,
|
||||||
|
global_state: &'a GlobalState,
|
||||||
|
|
||||||
|
#[conrod(common_builder)]
|
||||||
|
common: widget::CommonBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Group<'a> {
|
||||||
|
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
|
||||||
|
pub fn new(
|
||||||
|
show: &'a mut Show,
|
||||||
|
client: &'a Client,
|
||||||
|
settings: &'a Settings,
|
||||||
|
imgs: &'a Imgs,
|
||||||
|
fonts: &'a ConrodVoxygenFonts,
|
||||||
|
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
|
pulse: f32,
|
||||||
|
global_state: &'a GlobalState,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
show,
|
||||||
|
client,
|
||||||
|
settings,
|
||||||
|
imgs,
|
||||||
|
fonts,
|
||||||
|
localized_strings,
|
||||||
|
pulse,
|
||||||
|
global_state,
|
||||||
|
common: widget::CommonBuilder::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Accept,
|
||||||
|
Decline,
|
||||||
|
Kick(Uid),
|
||||||
|
LeaveGroup,
|
||||||
|
AssignLeader(Uid),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Group<'a> {
|
||||||
|
type Event = Vec<Event>;
|
||||||
|
type State = State;
|
||||||
|
type Style = ();
|
||||||
|
|
||||||
|
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
|
||||||
|
Self::State {
|
||||||
|
ids: Ids::new(id_gen),
|
||||||
|
selected_member: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> Self::Style {}
|
||||||
|
|
||||||
|
//TODO: Disband groups when there's only one member in them
|
||||||
|
//TODO: Always send health, energy, level and position of group members to the
|
||||||
|
// client
|
||||||
|
#[allow(clippy::unused_unit)] // TODO: Pending review in #587
|
||||||
|
#[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
|
||||||
|
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
||||||
|
let widget::UpdateArgs { state, ui, .. } = args;
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
// Don't show pets
|
||||||
|
let group_members = self
|
||||||
|
.client
|
||||||
|
.group_members()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(u, r)| match r {
|
||||||
|
Role::Member => Some(u),
|
||||||
|
Role::Pet => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// Not considered in group for ui purposes if it is just pets
|
||||||
|
let in_group = !group_members.is_empty();
|
||||||
|
if !in_group {
|
||||||
|
self.show.group_menu = false;
|
||||||
|
self.show.group = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) {
|
||||||
|
Some(player_info) => player_info
|
||||||
|
.character
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| format!("Player<{}>", uid), |c| c.name.clone()),
|
||||||
|
None => client
|
||||||
|
.state()
|
||||||
|
.ecs()
|
||||||
|
.entity_from_uid(uid.0)
|
||||||
|
.and_then(|entity| {
|
||||||
|
client
|
||||||
|
.state()
|
||||||
|
.ecs()
|
||||||
|
.read_storage::<Stats>()
|
||||||
|
.get(entity)
|
||||||
|
.map(|stats| stats.name.clone())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| format!("Npc<{}>", uid)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_invite = self.client.group_invite();
|
||||||
|
|
||||||
|
let my_uid = self.client.uid();
|
||||||
|
|
||||||
|
// TODO show something to the player when they click on the group button while
|
||||||
|
// they are not in a group so that it doesn't look like the button is
|
||||||
|
// broken
|
||||||
|
if self.show.group_menu || open_invite.is_some() {
|
||||||
|
// Frame
|
||||||
|
Rectangle::fill_with([220.0, 140.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8))
|
||||||
|
.bottom_left_with_margins_on(ui.window, 108.0, 490.0)
|
||||||
|
.crop_kids()
|
||||||
|
.set(state.ids.bg, ui);
|
||||||
|
}
|
||||||
|
if let Some((_, timeout_start, timeout_dur)) = open_invite {
|
||||||
|
// Group Menu button
|
||||||
|
Button::image(self.imgs.group_icon)
|
||||||
|
.w_h(49.0, 26.0)
|
||||||
|
.bottom_left_with_margins_on(ui.window, 10.0, 490.0)
|
||||||
|
.set(state.ids.group_button, ui);
|
||||||
|
// Show timeout bar
|
||||||
|
let timeout_progress =
|
||||||
|
1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32();
|
||||||
|
Image::new(self.imgs.progress_frame)
|
||||||
|
.w_h(100.0, 10.0)
|
||||||
|
.middle_of(state.ids.bg)
|
||||||
|
.color(Some(UI_MAIN))
|
||||||
|
.set(state.ids.timeout_bg, ui);
|
||||||
|
Image::new(self.imgs.progress)
|
||||||
|
.w_h(98.0 * timeout_progress as f64, 8.0)
|
||||||
|
.top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0)
|
||||||
|
.color(Some(UI_HIGHLIGHT_0))
|
||||||
|
.set(state.ids.timeout, ui);
|
||||||
|
}
|
||||||
|
// Buttons
|
||||||
|
if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) {
|
||||||
|
// Group Menu Button
|
||||||
|
if Button::image(if self.show.group_menu {
|
||||||
|
self.imgs.group_icon_press
|
||||||
|
} else {
|
||||||
|
self.imgs.group_icon
|
||||||
|
})
|
||||||
|
.w_h(49.0, 26.0)
|
||||||
|
.bottom_left_with_margins_on(ui.window, 10.0, 490.0)
|
||||||
|
.hover_image(self.imgs.group_icon_hover)
|
||||||
|
.press_image(self.imgs.group_icon_press)
|
||||||
|
.set(state.ids.group_button, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
self.show.group_menu = !self.show.group_menu;
|
||||||
|
};
|
||||||
|
Text::new(&group_name)
|
||||||
|
.up_from(state.ids.group_button, 5.0)
|
||||||
|
.font_size(14)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(BLACK)
|
||||||
|
.set(state.ids.title_bg, ui);
|
||||||
|
Text::new(&group_name)
|
||||||
|
.bottom_right_with_margins_on(state.ids.title_bg, 1.0, 1.0)
|
||||||
|
.font_size(14)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(state.ids.title, ui);
|
||||||
|
// Member panels
|
||||||
|
let group_size = group_members.len();
|
||||||
|
if state.ids.member_panels_bg.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_panels_bg
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.member_health.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_health
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.member_stam.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_stam
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.member_panels_frame.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_panels_frame
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.member_panels_txt.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_panels_txt
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.dead_txt.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.dead_txt
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.health_txt.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.health_txt
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.member_panels_txt_bg.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.member_panels_txt_bg
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let client_state = self.client.state();
|
||||||
|
let stats = client_state.ecs().read_storage::<common::comp::Stats>();
|
||||||
|
let energy = client_state.ecs().read_storage::<common::comp::Energy>();
|
||||||
|
let uid_allocator = client_state
|
||||||
|
.ecs()
|
||||||
|
.read_resource::<common::sync::UidAllocator>();
|
||||||
|
|
||||||
|
for (i, &uid) in group_members.iter().copied().enumerate() {
|
||||||
|
self.show.group = true;
|
||||||
|
let entity = uid_allocator.retrieve_entity_internal(uid.into());
|
||||||
|
let stats = entity.and_then(|entity| stats.get(entity));
|
||||||
|
let energy = entity.and_then(|entity| energy.get(entity));
|
||||||
|
if let Some(stats) = stats {
|
||||||
|
let char_name = stats.name.to_string();
|
||||||
|
let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64;
|
||||||
|
|
||||||
|
// change panel positions when debug info is shown
|
||||||
|
let offset = if self.global_state.settings.gameplay.toggle_debug {
|
||||||
|
290.0
|
||||||
|
} else {
|
||||||
|
110.0
|
||||||
|
};
|
||||||
|
let back = if i == 0 {
|
||||||
|
Image::new(self.imgs.member_bg)
|
||||||
|
.top_left_with_margins_on(ui.window, offset, 20.0)
|
||||||
|
} else {
|
||||||
|
Image::new(self.imgs.member_bg)
|
||||||
|
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
|
||||||
|
};
|
||||||
|
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
|
||||||
|
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
|
||||||
|
let health_col = match (health_perc * 100.0) as u8 {
|
||||||
|
0..=20 => crit_hp_color,
|
||||||
|
21..=40 => LOW_HP_COLOR,
|
||||||
|
_ => HP_COLOR,
|
||||||
|
};
|
||||||
|
let is_leader = uid == leader;
|
||||||
|
// Don't show panel for the player!
|
||||||
|
// Panel BG
|
||||||
|
back.w_h(152.0, 36.0)
|
||||||
|
.color(if is_leader {
|
||||||
|
Some(ERROR_COLOR)
|
||||||
|
} else {
|
||||||
|
Some(TEXT_COLOR)
|
||||||
|
})
|
||||||
|
.set(state.ids.member_panels_bg[i], ui);
|
||||||
|
// Health
|
||||||
|
Image::new(self.imgs.bar_content)
|
||||||
|
.w_h(148.0 * health_perc, 22.0)
|
||||||
|
.color(Some(health_col))
|
||||||
|
.top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0)
|
||||||
|
.set(state.ids.member_health[i], ui);
|
||||||
|
if stats.is_dead {
|
||||||
|
// Death Text
|
||||||
|
Text::new(&self.localized_strings.get("hud.group.dead"))
|
||||||
|
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0)
|
||||||
|
.font_size(20)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(KILL_COLOR)
|
||||||
|
.set(state.ids.dead_txt[i], ui);
|
||||||
|
} else {
|
||||||
|
// Health Text
|
||||||
|
let txt = format!(
|
||||||
|
"{}/{}",
|
||||||
|
stats.health.current() / 10 as u32,
|
||||||
|
stats.health.maximum() / 10 as u32,
|
||||||
|
);
|
||||||
|
// Change font size depending on health amount
|
||||||
|
let font_size = match stats.health.maximum() {
|
||||||
|
0..=999 => 14,
|
||||||
|
1000..=9999 => 13,
|
||||||
|
10000..=99999 => 12,
|
||||||
|
_ => 11,
|
||||||
|
};
|
||||||
|
// Change text offset depending on health amount
|
||||||
|
let txt_offset = match stats.health.maximum() {
|
||||||
|
0..=999 => 4.0,
|
||||||
|
1000..=9999 => 4.5,
|
||||||
|
10000..=99999 => 5.0,
|
||||||
|
_ => 5.5,
|
||||||
|
};
|
||||||
|
Text::new(&txt)
|
||||||
|
.mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset)
|
||||||
|
.font_size(font_size)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(Color::Rgba(1.0, 1.0, 1.0, 0.5))
|
||||||
|
.set(state.ids.health_txt[i], ui);
|
||||||
|
};
|
||||||
|
// Panel Frame
|
||||||
|
Image::new(self.imgs.member_frame)
|
||||||
|
.w_h(152.0, 36.0)
|
||||||
|
.middle_of(state.ids.member_panels_bg[i])
|
||||||
|
.color(Some(UI_HIGHLIGHT_0))
|
||||||
|
.set(state.ids.member_panels_frame[i], ui);
|
||||||
|
// Panel Text
|
||||||
|
Text::new(&char_name)
|
||||||
|
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
|
||||||
|
.font_size(20)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(BLACK)
|
||||||
|
.w(300.0) // limit name length display
|
||||||
|
.set(state.ids.member_panels_txt_bg[i], ui);
|
||||||
|
Text::new(&char_name)
|
||||||
|
.bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
|
||||||
|
.font_size(20)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
|
||||||
|
.w(300.0) // limit name length display
|
||||||
|
.set(state.ids.member_panels_txt[i], ui);
|
||||||
|
if let Some(energy) = energy {
|
||||||
|
let stam_perc = energy.current() as f64 / energy.maximum() as f64;
|
||||||
|
// Stamina
|
||||||
|
Image::new(self.imgs.bar_content)
|
||||||
|
.w_h(100.0 * stam_perc, 8.0)
|
||||||
|
.color(Some(MANA_COLOR))
|
||||||
|
.top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
|
||||||
|
.set(state.ids.member_stam[i], ui);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Values N.A.
|
||||||
|
if let Some(stats) = stats {
|
||||||
|
Text::new(&stats.name.to_string())
|
||||||
|
.top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
|
||||||
|
.font_size(20)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(GROUP_COLOR)
|
||||||
|
.set(state.ids.member_panels_txt[i], ui);
|
||||||
|
};
|
||||||
|
let offset = if self.global_state.settings.gameplay.toggle_debug {
|
||||||
|
210.0
|
||||||
|
} else {
|
||||||
|
110.0
|
||||||
|
};
|
||||||
|
let back = if i == 0 {
|
||||||
|
Image::new(self.imgs.member_bg)
|
||||||
|
.top_left_with_margins_on(ui.window, offset, 20.0)
|
||||||
|
} else {
|
||||||
|
Image::new(self.imgs.member_bg)
|
||||||
|
.down_from(state.ids.member_panels_bg[i - 1], 40.0)
|
||||||
|
};
|
||||||
|
back.w_h(152.0, 36.0)
|
||||||
|
.color(Some(TEXT_COLOR))
|
||||||
|
.set(state.ids.member_panels_bg[i], ui);
|
||||||
|
// Panel Frame
|
||||||
|
Image::new(self.imgs.member_frame)
|
||||||
|
.w_h(152.0, 36.0)
|
||||||
|
.middle_of(state.ids.member_panels_bg[i])
|
||||||
|
.color(Some(UI_HIGHLIGHT_0))
|
||||||
|
.set(state.ids.member_panels_frame[i], ui);
|
||||||
|
// Panel Text
|
||||||
|
Text::new(&self.localized_strings.get("hud.group.out_of_range"))
|
||||||
|
.mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
|
||||||
|
.font_size(16)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(state.ids.dead_txt[i], ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show.group_menu {
|
||||||
|
let selected = state.selected_member;
|
||||||
|
if Button::image(self.imgs.button) // Change button behaviour and style when the friendslist is working
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.top_right_with_margins_on(state.ids.bg, 5.0, 5.0)
|
||||||
|
.hover_image(self.imgs.button)
|
||||||
|
.press_image(self.imgs.button)
|
||||||
|
.label_color(TEXT_COLOR_GREY)
|
||||||
|
.image_color(TEXT_COLOR_GREY)
|
||||||
|
.label(&self.localized_strings.get("hud.group.add_friend"))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(10))
|
||||||
|
.set(state.ids.btn_friend, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{};
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label(&self.localized_strings.get("hud.group.leave"))
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(10))
|
||||||
|
.set(state.ids.btn_leave, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
self.show.group_menu = false;
|
||||||
|
self.show.group = !self.show.group;
|
||||||
|
events.push(Event::LeaveGroup);
|
||||||
|
};
|
||||||
|
// Group leader functions
|
||||||
|
if my_uid == Some(leader) {
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.mid_bottom_with_margin_on(state.ids.btn_friend, -27.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label(&self.localized_strings.get("hud.group.assign_leader"))
|
||||||
|
.label_color(if state.selected_member.is_some() {
|
||||||
|
TEXT_COLOR
|
||||||
|
} else {
|
||||||
|
TEXT_COLOR_GREY
|
||||||
|
})
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(10))
|
||||||
|
.set(state.ids.btn_leader, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
if let Some(uid) = selected {
|
||||||
|
events.push(Event::AssignLeader(uid));
|
||||||
|
state.update(|s| {
|
||||||
|
s.selected_member = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.mid_bottom_with_margin_on(state.ids.btn_leader, -27.0)
|
||||||
|
.hover_image(self.imgs.button)
|
||||||
|
.press_image(self.imgs.button)
|
||||||
|
.label(&self.localized_strings.get("hud.group.link_group"))
|
||||||
|
.hover_image(self.imgs.button)
|
||||||
|
.press_image(self.imgs.button)
|
||||||
|
.label_color(TEXT_COLOR_GREY)
|
||||||
|
.image_color(TEXT_COLOR_GREY)
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(10))
|
||||||
|
.set(state.ids.btn_link, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{};
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.mid_bottom_with_margin_on(state.ids.btn_link, -27.0)
|
||||||
|
.down_from(state.ids.btn_link, 5.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label(&self.localized_strings.get("hud.group.kick"))
|
||||||
|
.label_color(if state.selected_member.is_some() {
|
||||||
|
TEXT_COLOR
|
||||||
|
} else {
|
||||||
|
TEXT_COLOR_GREY
|
||||||
|
})
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(10))
|
||||||
|
.set(state.ids.btn_kick, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
if let Some(uid) = selected {
|
||||||
|
events.push(Event::Kick(uid));
|
||||||
|
state.update(|s| {
|
||||||
|
s.selected_member = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Group Members, only character names, cut long names when they exceed the
|
||||||
|
// button size
|
||||||
|
let group_size = group_members.len();
|
||||||
|
if state.ids.members.len() < group_size {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.members
|
||||||
|
.resize(group_size, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Scrollable area for group member names
|
||||||
|
Rectangle::fill_with([110.0, 135.0], color::TRANSPARENT)
|
||||||
|
.top_left_with_margins_on(state.ids.bg, 5.0, 5.0)
|
||||||
|
.crop_kids()
|
||||||
|
.scroll_kids_vertically()
|
||||||
|
.set(state.ids.scroll_area, ui);
|
||||||
|
Scrollbar::y_axis(state.ids.scroll_area)
|
||||||
|
.thickness(5.0)
|
||||||
|
.rgba(0.33, 0.33, 0.33, 1.0)
|
||||||
|
.set(state.ids.scrollbar, ui);
|
||||||
|
// List member names
|
||||||
|
for (i, &uid) in group_members.iter().copied().enumerate() {
|
||||||
|
let selected = state.selected_member.map_or(false, |u| u == uid);
|
||||||
|
let char_name = uid_to_name_text(uid, &self.client);
|
||||||
|
// TODO: Do something special visually if uid == leader
|
||||||
|
if Button::image(if selected {
|
||||||
|
self.imgs.selection
|
||||||
|
} else {
|
||||||
|
self.imgs.nothing
|
||||||
|
})
|
||||||
|
.w_h(100.0, 22.0)
|
||||||
|
.and(|w| {
|
||||||
|
if i == 0 {
|
||||||
|
w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0)
|
||||||
|
} else {
|
||||||
|
w.down_from(state.ids.members[i - 1], 5.0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.hover_image(self.imgs.selection_hover)
|
||||||
|
.press_image(self.imgs.selection_press)
|
||||||
|
.crop_kids()
|
||||||
|
.label_x(Relative::Place(Place::Start(Some(4.0))))
|
||||||
|
.label(&char_name)
|
||||||
|
.label_color(if uid == leader {
|
||||||
|
ERROR_COLOR
|
||||||
|
} else {
|
||||||
|
TEXT_COLOR
|
||||||
|
})
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(12))
|
||||||
|
.set(state.ids.members[i], ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
// Do nothing when clicking yourself
|
||||||
|
if Some(uid) != my_uid {
|
||||||
|
// Select the group member
|
||||||
|
state.update(|s| {
|
||||||
|
s.selected_member = if selected { None } else { Some(uid) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Maximum of 6 Players/Npcs per Group
|
||||||
|
// Player pets count as group members, too. They are not counted
|
||||||
|
// into the maximum group size.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((invite_uid, _, _)) = open_invite {
|
||||||
|
self.show.group = true; // Auto open group menu
|
||||||
|
// TODO: add group name here too
|
||||||
|
// Invite text
|
||||||
|
|
||||||
|
let name = uid_to_name_text(invite_uid, &self.client);
|
||||||
|
let invite_text = self
|
||||||
|
.localized_strings
|
||||||
|
.get("hud.group.invite_to_join")
|
||||||
|
.replace("{name}", &name);
|
||||||
|
Text::new(&invite_text)
|
||||||
|
.mid_top_with_margin_on(state.ids.bg, 5.0)
|
||||||
|
.font_size(12)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.w(165.0) // Text stays within frame
|
||||||
|
.set(state.ids.title, ui);
|
||||||
|
// Accept Button
|
||||||
|
let accept_key = self
|
||||||
|
.settings
|
||||||
|
.controls
|
||||||
|
.get_binding(GameInput::AcceptGroupInvite)
|
||||||
|
.map_or_else(|| "".into(), |key| key.to_string());
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label(&format!(
|
||||||
|
"[{}] {}",
|
||||||
|
&accept_key,
|
||||||
|
&self.localized_strings.get("common.accept")
|
||||||
|
))
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(12))
|
||||||
|
.set(state.ids.btn_accept, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
events.push(Event::Accept);
|
||||||
|
self.show.group_menu = true;
|
||||||
|
};
|
||||||
|
// Decline button
|
||||||
|
let decline_key = self
|
||||||
|
.settings
|
||||||
|
.controls
|
||||||
|
.get_binding(GameInput::DeclineGroupInvite)
|
||||||
|
.map_or_else(|| "".into(), |key| key.to_string());
|
||||||
|
if Button::image(self.imgs.button)
|
||||||
|
.w_h(90.0, 22.0)
|
||||||
|
.bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0)
|
||||||
|
.hover_image(self.imgs.button_hover)
|
||||||
|
.press_image(self.imgs.button_press)
|
||||||
|
.label(&format!(
|
||||||
|
"[{}] {}",
|
||||||
|
&decline_key,
|
||||||
|
&self.localized_strings.get("common.decline")
|
||||||
|
))
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(12))
|
||||||
|
.set(state.ids.btn_decline, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
events.push(Event::Decline);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
}
|
@ -59,14 +59,19 @@ image_ids! {
|
|||||||
selection_press: "voxygen.element.frames.selection_press",
|
selection_press: "voxygen.element.frames.selection_press",
|
||||||
|
|
||||||
// Social Window
|
// Social Window
|
||||||
social_button: "voxygen.element.buttons.social_tab",
|
social_frame_on: "voxygen.element.misc_bg.social_frame",
|
||||||
social_button_pressed: "voxygen.element.buttons.social_tab_pressed",
|
social_bg_on: "voxygen.element.misc_bg.social_bg",
|
||||||
social_button_hover: "voxygen.element.buttons.social_tab_hover",
|
social_frame_friends: "voxygen.element.misc_bg.social_frame",
|
||||||
social_button_press: "voxygen.element.buttons.social_tab_press",
|
social_bg_friends: "voxygen.element.misc_bg.social_bg",
|
||||||
social_frame: "voxygen.element.frames.social_frame",
|
social_frame_fact: "voxygen.element.misc_bg.social_frame",
|
||||||
|
social_bg_fact: "voxygen.element.misc_bg.social_bg",
|
||||||
|
social_tab_act: "voxygen.element.buttons.social_tab_active",
|
||||||
|
social_tab_online: "voxygen.element.misc_bg.social_tab_online",
|
||||||
|
social_tab_inact: "voxygen.element.buttons.social_tab_inactive",
|
||||||
|
social_tab_inact_hover: "voxygen.element.buttons.social_tab_inactive",
|
||||||
|
social_tab_inact_press: "voxygen.element.buttons.social_tab_inactive",
|
||||||
|
|
||||||
// Crafting Window
|
// Crafting Window
|
||||||
|
|
||||||
crafting_window: "voxygen.element.misc_bg.crafting",
|
crafting_window: "voxygen.element.misc_bg.crafting",
|
||||||
crafting_frame: "voxygen.element.misc_bg.crafting_frame",
|
crafting_frame: "voxygen.element.misc_bg.crafting_frame",
|
||||||
crafting_icon_bordered: "voxygen.element.icons.anvil",
|
crafting_icon_bordered: "voxygen.element.icons.anvil",
|
||||||
@ -74,6 +79,9 @@ image_ids! {
|
|||||||
crafting_icon_hover: "voxygen.element.buttons.anvil_hover",
|
crafting_icon_hover: "voxygen.element.buttons.anvil_hover",
|
||||||
crafting_icon_press: "voxygen.element.buttons.anvil_press",
|
crafting_icon_press: "voxygen.element.buttons.anvil_press",
|
||||||
|
|
||||||
|
// Group Window
|
||||||
|
member_frame: "voxygen.element.frames.group_member_frame",
|
||||||
|
member_bg: "voxygen.element.frames.group_member_bg",
|
||||||
|
|
||||||
// Chat-Arrows
|
// Chat-Arrows
|
||||||
chat_arrow: "voxygen.element.buttons.arrow_down",
|
chat_arrow: "voxygen.element.buttons.arrow_down",
|
||||||
@ -94,7 +102,6 @@ image_ids! {
|
|||||||
slider_indicator_small: "voxygen.element.slider.indicator_round",
|
slider_indicator_small: "voxygen.element.slider.indicator_round",
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
|
||||||
settings: "voxygen.element.buttons.settings",
|
settings: "voxygen.element.buttons.settings",
|
||||||
settings_hover: "voxygen.element.buttons.settings_hover",
|
settings_hover: "voxygen.element.buttons.settings_hover",
|
||||||
settings_press: "voxygen.element.buttons.settings_press",
|
settings_press: "voxygen.element.buttons.settings_press",
|
||||||
@ -111,6 +118,10 @@ image_ids! {
|
|||||||
spellbook_hover: "voxygen.element.buttons.spellbook_hover",
|
spellbook_hover: "voxygen.element.buttons.spellbook_hover",
|
||||||
spellbook_press: "voxygen.element.buttons.spellbook_press",
|
spellbook_press: "voxygen.element.buttons.spellbook_press",
|
||||||
|
|
||||||
|
group_icon: "voxygen.element.buttons.group",
|
||||||
|
group_icon_hover: "voxygen.element.buttons.group_hover",
|
||||||
|
group_icon_press: "voxygen.element.buttons.group_press",
|
||||||
|
|
||||||
// Skill Icons
|
// Skill Icons
|
||||||
twohsword_m1: "voxygen.element.icons.2hsword_m1",
|
twohsword_m1: "voxygen.element.icons.2hsword_m1",
|
||||||
twohsword_m2: "voxygen.element.icons.2hsword_m2",
|
twohsword_m2: "voxygen.element.icons.2hsword_m2",
|
||||||
|
@ -3,6 +3,7 @@ mod buttons;
|
|||||||
mod chat;
|
mod chat;
|
||||||
mod crafting;
|
mod crafting;
|
||||||
mod esc_menu;
|
mod esc_menu;
|
||||||
|
mod group;
|
||||||
mod hotbar;
|
mod hotbar;
|
||||||
mod img_ids;
|
mod img_ids;
|
||||||
mod item_imgs;
|
mod item_imgs;
|
||||||
@ -30,6 +31,7 @@ use chat::Chat;
|
|||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use crafting::Crafting;
|
use crafting::Crafting;
|
||||||
use esc_menu::EscMenu;
|
use esc_menu::EscMenu;
|
||||||
|
use group::Group;
|
||||||
use img_ids::Imgs;
|
use img_ids::Imgs;
|
||||||
use item_imgs::ItemImgs;
|
use item_imgs::ItemImgs;
|
||||||
use map::Map;
|
use map::Map;
|
||||||
@ -69,7 +71,7 @@ const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
|||||||
const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0);
|
const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0);
|
||||||
const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0);
|
const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0);
|
||||||
const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
||||||
//const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5);
|
const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5);
|
||||||
const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
|
const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
|
||||||
//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
|
||||||
const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
|
const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
|
||||||
@ -80,6 +82,7 @@ const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
|
|||||||
const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0);
|
const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0);
|
||||||
const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
|
const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
|
||||||
const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
|
const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
|
||||||
|
//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0);
|
||||||
//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
|
//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
|
||||||
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
|
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
|
||||||
|
|
||||||
@ -109,12 +112,18 @@ const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0);
|
|||||||
/// Color for collected loot messages
|
/// Color for collected loot messages
|
||||||
const LOOT_COLOR: Color = Color::Rgba(0.69, 0.57, 1.0, 1.0);
|
const LOOT_COLOR: Color = Color::Rgba(0.69, 0.57, 1.0, 1.0);
|
||||||
|
|
||||||
|
//Nametags
|
||||||
|
const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
|
||||||
|
const DEFAULT_NPC: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
// UI Color-Theme
|
// UI Color-Theme
|
||||||
const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
|
const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
|
||||||
//const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark
|
//const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark
|
||||||
const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);
|
const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);
|
||||||
//const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0);
|
//const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0);
|
||||||
|
|
||||||
|
/// Distance at which nametags are visible for group members
|
||||||
|
const NAMETAG_GROUP_RANGE: f32 = 300.0;
|
||||||
/// Distance at which nametags are visible
|
/// Distance at which nametags are visible
|
||||||
const NAMETAG_RANGE: f32 = 40.0;
|
const NAMETAG_RANGE: f32 = 40.0;
|
||||||
/// Time nametags stay visible after doing damage even if they are out of range
|
/// Time nametags stay visible after doing damage even if they are out of range
|
||||||
@ -208,6 +217,7 @@ widget_ids! {
|
|||||||
social_window,
|
social_window,
|
||||||
crafting_window,
|
crafting_window,
|
||||||
settings_window,
|
settings_window,
|
||||||
|
group_window,
|
||||||
|
|
||||||
// Free look indicator
|
// Free look indicator
|
||||||
free_look_txt,
|
free_look_txt,
|
||||||
@ -242,6 +252,8 @@ pub struct DebugInfo {
|
|||||||
pub struct HudInfo {
|
pub struct HudInfo {
|
||||||
pub is_aiming: bool,
|
pub is_aiming: bool,
|
||||||
pub is_first_person: bool,
|
pub is_first_person: bool,
|
||||||
|
pub target_entity: Option<specs::Entity>,
|
||||||
|
pub selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -297,6 +309,12 @@ pub enum Event {
|
|||||||
ChangeAutoWalkBehavior(PressBehavior),
|
ChangeAutoWalkBehavior(PressBehavior),
|
||||||
ChangeStopAutoWalkOnInput(bool),
|
ChangeStopAutoWalkOnInput(bool),
|
||||||
CraftRecipe(String),
|
CraftRecipe(String),
|
||||||
|
InviteMember(common::sync::Uid),
|
||||||
|
AcceptInvite,
|
||||||
|
DeclineInvite,
|
||||||
|
KickMember(common::sync::Uid),
|
||||||
|
LeaveGroup,
|
||||||
|
AssignLeader(common::sync::Uid),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Are these the possible layouts we want?
|
// TODO: Are these the possible layouts we want?
|
||||||
@ -352,6 +370,8 @@ pub struct Show {
|
|||||||
bag: bool,
|
bag: bool,
|
||||||
social: bool,
|
social: bool,
|
||||||
spell: bool,
|
spell: bool,
|
||||||
|
group: bool,
|
||||||
|
group_menu: bool,
|
||||||
esc_menu: bool,
|
esc_menu: bool,
|
||||||
open_windows: Windows,
|
open_windows: Windows,
|
||||||
map: bool,
|
map: bool,
|
||||||
@ -389,7 +409,6 @@ impl Show {
|
|||||||
fn social(&mut self, open: bool) {
|
fn social(&mut self, open: bool) {
|
||||||
if !self.esc_menu {
|
if !self.esc_menu {
|
||||||
self.social = open;
|
self.social = open;
|
||||||
self.crafting = false;
|
|
||||||
self.spell = false;
|
self.spell = false;
|
||||||
self.want_grab = !open;
|
self.want_grab = !open;
|
||||||
}
|
}
|
||||||
@ -489,7 +508,7 @@ impl Show {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_social(&mut self) {
|
fn toggle_social(&mut self) {
|
||||||
self.social = !self.social;
|
self.social(!self.social);
|
||||||
self.spell = false;
|
self.spell = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,6 +617,8 @@ impl Hud {
|
|||||||
ui: true,
|
ui: true,
|
||||||
social: false,
|
social: false,
|
||||||
spell: false,
|
spell: false,
|
||||||
|
group: false,
|
||||||
|
group_menu: false,
|
||||||
mini_map: true,
|
mini_map: true,
|
||||||
settings_tab: SettingsTab::Interface,
|
settings_tab: SettingsTab::Interface,
|
||||||
social_tab: SocialTab::Online,
|
social_tab: SocialTab::Online,
|
||||||
@ -1032,7 +1053,7 @@ impl Hud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render overhead name tags and health bars
|
// Render overhead name tags and health bars
|
||||||
for (pos, name, stats, energy, height_offset, hpfl, uid) in (
|
for (pos, name, stats, energy, height_offset, hpfl, uid, in_group) in (
|
||||||
&entities,
|
&entities,
|
||||||
&pos,
|
&pos,
|
||||||
interpolated.maybe(),
|
interpolated.maybe(),
|
||||||
@ -1045,11 +1066,34 @@ impl Hud {
|
|||||||
&uids,
|
&uids,
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
.filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
|
.map(|(a, b, c, d, e, f, g, h, i, uid)| {
|
||||||
// Don't show outside a certain range
|
(
|
||||||
.filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| {
|
a,
|
||||||
pos.0.distance_squared(player_pos)
|
b,
|
||||||
< (if hpfl
|
c,
|
||||||
|
d,
|
||||||
|
e,
|
||||||
|
f,
|
||||||
|
g,
|
||||||
|
h,
|
||||||
|
i,
|
||||||
|
uid,
|
||||||
|
client.group_members().contains_key(uid),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|(entity, pos, _, stats, _, _, _, _, hpfl, _, in_group)| {
|
||||||
|
*entity != me && !stats.is_dead
|
||||||
|
&& (stats.health.current() != stats.health.maximum()
|
||||||
|
|| info.target_entity.map_or(false, |e| e == *entity)
|
||||||
|
|| info.selected_entity.map_or(false, |s| s.0 == *entity)
|
||||||
|
|| *in_group
|
||||||
|
)
|
||||||
|
// Don't show outside a certain range
|
||||||
|
&& pos.0.distance_squared(player_pos)
|
||||||
|
< (if *in_group
|
||||||
|
{
|
||||||
|
NAMETAG_GROUP_RANGE
|
||||||
|
} else if hpfl
|
||||||
.time_since_last_dmg_by_me
|
.time_since_last_dmg_by_me
|
||||||
.map_or(false, |t| t < NAMETAG_DMG_TIME)
|
.map_or(false, |t| t < NAMETAG_DMG_TIME)
|
||||||
{
|
{
|
||||||
@ -1059,25 +1103,40 @@ impl Hud {
|
|||||||
})
|
})
|
||||||
.powi(2)
|
.powi(2)
|
||||||
})
|
})
|
||||||
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, uid)| {
|
.map(
|
||||||
// TODO: This is temporary
|
|(
|
||||||
// If the player used the default character name display their name instead
|
_,
|
||||||
let name = if stats.name == "Character Name" {
|
pos,
|
||||||
player.map_or(&stats.name, |p| &p.alias)
|
interpolated,
|
||||||
} else {
|
|
||||||
&stats.name
|
|
||||||
};
|
|
||||||
(
|
|
||||||
interpolated.map_or(pos.0, |i| i.pos),
|
|
||||||
name,
|
|
||||||
stats,
|
stats,
|
||||||
energy,
|
energy,
|
||||||
// TODO: when body.height() is more accurate remove the 2.0
|
player,
|
||||||
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
|
scale,
|
||||||
|
body,
|
||||||
hpfl,
|
hpfl,
|
||||||
uid,
|
uid,
|
||||||
)
|
in_group,
|
||||||
})
|
)| {
|
||||||
|
// TODO: This is temporary
|
||||||
|
// If the player used the default character name display their name instead
|
||||||
|
let name = if stats.name == "Character Name" {
|
||||||
|
player.map_or(&stats.name, |p| &p.alias)
|
||||||
|
} else {
|
||||||
|
&stats.name
|
||||||
|
};
|
||||||
|
(
|
||||||
|
interpolated.map_or(pos.0, |i| i.pos),
|
||||||
|
name,
|
||||||
|
stats,
|
||||||
|
energy,
|
||||||
|
// TODO: when body.height() is more accurate remove the 2.0
|
||||||
|
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
|
||||||
|
hpfl,
|
||||||
|
uid,
|
||||||
|
in_group,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
{
|
{
|
||||||
let bubble = self.speech_bubbles.get(uid);
|
let bubble = self.speech_bubbles.get(uid);
|
||||||
|
|
||||||
@ -1094,6 +1153,7 @@ impl Hud {
|
|||||||
stats,
|
stats,
|
||||||
energy,
|
energy,
|
||||||
own_level,
|
own_level,
|
||||||
|
in_group,
|
||||||
&global_state.settings.gameplay,
|
&global_state.settings.gameplay,
|
||||||
self.pulse,
|
self.pulse,
|
||||||
&self.voxygen_i18n,
|
&self.voxygen_i18n,
|
||||||
@ -1863,23 +1923,56 @@ impl Hud {
|
|||||||
|
|
||||||
// Social Window
|
// Social Window
|
||||||
if self.show.social {
|
if self.show.social {
|
||||||
for event in Social::new(
|
let ecs = client.state().ecs();
|
||||||
&self.show,
|
let _stats = ecs.read_storage::<comp::Stats>();
|
||||||
client,
|
let me = client.entity();
|
||||||
&self.imgs,
|
if let Some(_stats) = stats.get(me) {
|
||||||
&self.fonts,
|
for event in Social::new(
|
||||||
&self.voxygen_i18n,
|
&self.show,
|
||||||
)
|
client,
|
||||||
.set(self.ids.social_window, ui_widgets)
|
&self.imgs,
|
||||||
{
|
&self.fonts,
|
||||||
match event {
|
&self.voxygen_i18n,
|
||||||
social::Event::Close => self.show.social(false),
|
info.selected_entity,
|
||||||
social::Event::ChangeSocialTab(social_tab) => {
|
&self.rot_imgs,
|
||||||
self.show.open_social_tab(social_tab)
|
tooltip_manager,
|
||||||
},
|
)
|
||||||
|
.set(self.ids.social_window, ui_widgets)
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
social::Event::Close => {
|
||||||
|
self.show.social(false);
|
||||||
|
self.force_ungrab = true;
|
||||||
|
},
|
||||||
|
social::Event::ChangeSocialTab(social_tab) => {
|
||||||
|
self.show.open_social_tab(social_tab)
|
||||||
|
},
|
||||||
|
social::Event::Invite(uid) => events.push(Event::InviteMember(uid)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Group Window
|
||||||
|
for event in Group::new(
|
||||||
|
&mut self.show,
|
||||||
|
client,
|
||||||
|
&global_state.settings,
|
||||||
|
&self.imgs,
|
||||||
|
&self.fonts,
|
||||||
|
&self.voxygen_i18n,
|
||||||
|
self.pulse,
|
||||||
|
&global_state,
|
||||||
|
)
|
||||||
|
.set(self.ids.group_window, ui_widgets)
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
group::Event::Accept => events.push(Event::AcceptInvite),
|
||||||
|
group::Event::Decline => events.push(Event::DeclineInvite),
|
||||||
|
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
|
||||||
|
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
|
||||||
|
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Spellbook
|
// Spellbook
|
||||||
if self.show.spell {
|
if self.show.spell {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::{
|
use super::{
|
||||||
img_ids::Imgs, FACTION_COLOR, GROUP_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, REGION_COLOR,
|
img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR,
|
||||||
SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
|
MANA_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
i18n::VoxygenLocalization,
|
i18n::VoxygenLocalization,
|
||||||
@ -42,6 +42,7 @@ widget_ids! {
|
|||||||
level_skull,
|
level_skull,
|
||||||
health_bar,
|
health_bar,
|
||||||
health_bar_bg,
|
health_bar_bg,
|
||||||
|
health_txt,
|
||||||
mana_bar,
|
mana_bar,
|
||||||
health_bar_fg,
|
health_bar_fg,
|
||||||
}
|
}
|
||||||
@ -56,11 +57,13 @@ pub struct Overhead<'a> {
|
|||||||
stats: &'a Stats,
|
stats: &'a Stats,
|
||||||
energy: Option<&'a Energy>,
|
energy: Option<&'a Energy>,
|
||||||
own_level: u32,
|
own_level: u32,
|
||||||
|
in_group: bool,
|
||||||
settings: &'a GameplaySettings,
|
settings: &'a GameplaySettings,
|
||||||
pulse: f32,
|
pulse: f32,
|
||||||
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
|
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
imgs: &'a Imgs,
|
imgs: &'a Imgs,
|
||||||
fonts: &'a ConrodVoxygenFonts,
|
fonts: &'a ConrodVoxygenFonts,
|
||||||
|
|
||||||
#[conrod(common_builder)]
|
#[conrod(common_builder)]
|
||||||
common: widget::CommonBuilder,
|
common: widget::CommonBuilder,
|
||||||
}
|
}
|
||||||
@ -73,6 +76,7 @@ impl<'a> Overhead<'a> {
|
|||||||
stats: &'a Stats,
|
stats: &'a Stats,
|
||||||
energy: Option<&'a Energy>,
|
energy: Option<&'a Energy>,
|
||||||
own_level: u32,
|
own_level: u32,
|
||||||
|
in_group: bool,
|
||||||
settings: &'a GameplaySettings,
|
settings: &'a GameplaySettings,
|
||||||
pulse: f32,
|
pulse: f32,
|
||||||
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
|
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
@ -85,6 +89,7 @@ impl<'a> Overhead<'a> {
|
|||||||
stats,
|
stats,
|
||||||
energy,
|
energy,
|
||||||
own_level,
|
own_level,
|
||||||
|
in_group,
|
||||||
settings,
|
settings,
|
||||||
pulse,
|
pulse,
|
||||||
voxygen_i18n,
|
voxygen_i18n,
|
||||||
@ -104,13 +109,20 @@ impl<'a> Ingameable for Overhead<'a> {
|
|||||||
// Number of conrod primitives contained in the overhead display. TODO maybe
|
// Number of conrod primitives contained in the overhead display. TODO maybe
|
||||||
// this could be done automatically?
|
// this could be done automatically?
|
||||||
// - 2 Text::new for name
|
// - 2 Text::new for name
|
||||||
|
// If HP Info is shown + 6
|
||||||
// - 1 for level: either Text or Image
|
// - 1 for level: either Text or Image
|
||||||
// - 4 for HP + mana + fg + bg
|
// - 4 for HP + mana + fg + bg
|
||||||
// If there's a speech bubble
|
// - 1 for HP Text
|
||||||
// - 2 Text::new for speech bubble
|
// If there's a speech bubble + 13
|
||||||
|
// - 2 Text::new for speec8 bubble
|
||||||
// - 1 Image::new for icon
|
// - 1 Image::new for icon
|
||||||
// - 10 Image::new for speech bubble (9-slice + tail)
|
// - 10 Image::new for speech bubble (9-slice + tail)
|
||||||
7 + if self.bubble.is_some() { 13 } else { 0 }
|
2 + if self.bubble.is_some() { 13 } else { 0 }
|
||||||
|
+ if (self.stats.health.current() as f64 / self.stats.health.maximum() as f64) < 1.0 {
|
||||||
|
6
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,19 +146,35 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
const BARSIZE: f64 = 2.0;
|
const BARSIZE: f64 = 2.0;
|
||||||
const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
|
const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
|
||||||
const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
|
const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
|
||||||
|
let hp_percentage =
|
||||||
|
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
||||||
|
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
|
||||||
|
let name_y = if hp_percentage.abs() > 99.9 {
|
||||||
|
MANA_BAR_Y + 20.0
|
||||||
|
} else if level_comp > 9 {
|
||||||
|
MANA_BAR_Y + 38.0
|
||||||
|
} else {
|
||||||
|
MANA_BAR_Y + 32.0
|
||||||
|
};
|
||||||
|
let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
|
||||||
// Name
|
// Name
|
||||||
Text::new(&self.name)
|
Text::new(&self.name)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(30)
|
.font_size(font_size)
|
||||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||||
.x_y(-1.0, MANA_BAR_Y + 48.0)
|
.x_y(-1.0, name_y)
|
||||||
|
.parent(id)
|
||||||
.set(state.ids.name_bg, ui);
|
.set(state.ids.name_bg, ui);
|
||||||
Text::new(&self.name)
|
Text::new(&self.name)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(30)
|
.font_size(font_size)
|
||||||
.color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
|
.color(if self.in_group {
|
||||||
.x_y(0.0, MANA_BAR_Y + 50.0)
|
GROUP_MEMBER
|
||||||
|
} else {
|
||||||
|
DEFAULT_NPC
|
||||||
|
})
|
||||||
|
.x_y(0.0, name_y + 1.0)
|
||||||
|
.parent(id)
|
||||||
.set(state.ids.name, ui);
|
.set(state.ids.name, ui);
|
||||||
|
|
||||||
// Speech bubble
|
// Speech bubble
|
||||||
@ -160,7 +188,7 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
.color(text_color)
|
.color(text_color)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(18)
|
.font_size(18)
|
||||||
.up_from(state.ids.name, 20.0)
|
.up_from(state.ids.name, 26.0)
|
||||||
.x_align_to(state.ids.name, Align::Middle)
|
.x_align_to(state.ids.name, Align::Middle)
|
||||||
.parent(id);
|
.parent(id);
|
||||||
|
|
||||||
@ -295,101 +323,121 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
.set(state.ids.speech_bubble_icon, ui);
|
.set(state.ids.speech_bubble_icon, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hp_percentage =
|
if hp_percentage < 100.0 {
|
||||||
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
// Show HP Bar
|
||||||
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
|
let hp_percentage =
|
||||||
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
|
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
||||||
|
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
|
||||||
|
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
Image::new(self.imgs.enemy_health_bg)
|
Image::new(self.imgs.enemy_health_bg)
|
||||||
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
|
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
|
||||||
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
|
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
|
||||||
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
|
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
|
||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.health_bar_bg, ui);
|
.set(state.ids.health_bar_bg, ui);
|
||||||
|
|
||||||
// % HP Filling
|
// % HP Filling
|
||||||
Image::new(self.imgs.enemy_bar)
|
Image::new(self.imgs.enemy_bar)
|
||||||
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
|
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
|
||||||
.x_y(
|
.x_y(
|
||||||
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
|
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
|
||||||
MANA_BAR_Y + 7.5,
|
MANA_BAR_Y + 7.5,
|
||||||
)
|
)
|
||||||
.color(Some(if hp_percentage <= 25.0 {
|
.color(Some(if hp_percentage <= 25.0 {
|
||||||
crit_hp_color
|
crit_hp_color
|
||||||
} else if hp_percentage <= 50.0 {
|
} else if hp_percentage <= 50.0 {
|
||||||
LOW_HP_COLOR
|
LOW_HP_COLOR
|
||||||
} else {
|
} else {
|
||||||
HP_COLOR
|
HP_COLOR
|
||||||
}))
|
}))
|
||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.health_bar, ui);
|
.set(state.ids.health_bar, ui);
|
||||||
|
// TODO Only show health values for entities below 100% health
|
||||||
|
let mut txt = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.stats.health.current().max(1) / 10 as u32, /* Don't show 0 health for
|
||||||
|
* living entities */
|
||||||
|
self.stats.health.maximum() / 10 as u32,
|
||||||
|
);
|
||||||
|
if self.stats.is_dead {
|
||||||
|
txt = self.voxygen_i18n.get("hud.group.dead").to_string()
|
||||||
|
};
|
||||||
|
Text::new(&txt)
|
||||||
|
.mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
|
||||||
|
.font_size(10)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.parent(id)
|
||||||
|
.set(state.ids.health_txt, ui);
|
||||||
|
|
||||||
// % Mana Filling
|
// % Mana Filling
|
||||||
if let Some(energy) = self.energy {
|
if let Some(energy) = self.energy {
|
||||||
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
|
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
|
||||||
|
|
||||||
Rectangle::fill_with(
|
Rectangle::fill_with(
|
||||||
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
|
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
|
||||||
MANA_COLOR,
|
MANA_COLOR,
|
||||||
)
|
)
|
||||||
.x_y(
|
.x_y(
|
||||||
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,
|
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,
|
||||||
MANA_BAR_Y, //-32.0,
|
MANA_BAR_Y, //-32.0,
|
||||||
)
|
)
|
||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.mana_bar, ui);
|
.set(state.ids.mana_bar, ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreground
|
// Foreground
|
||||||
Image::new(self.imgs.enemy_health)
|
Image::new(self.imgs.enemy_health)
|
||||||
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
|
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
|
||||||
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
|
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
|
||||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
|
||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.health_bar_fg, ui);
|
.set(state.ids.health_bar_fg, ui);
|
||||||
|
|
||||||
// Level
|
// Level
|
||||||
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
|
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
|
||||||
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
|
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
|
||||||
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
// Change visuals of the level display depending on the player level/opponent
|
// Change visuals of the level display depending on the player level/opponent
|
||||||
// level
|
// level
|
||||||
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
|
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
|
||||||
// + 10 level above player -> skull
|
// + 10 level above player -> skull
|
||||||
// + 5-10 levels above player -> high
|
// + 5-10 levels above player -> high
|
||||||
// -5 - +5 levels around player level -> equal
|
// -5 - +5 levels around player level -> equal
|
||||||
// - 5 levels below player -> low
|
// - 5 levels below player -> low
|
||||||
if level_comp > 9 {
|
if level_comp > 9 {
|
||||||
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
|
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
|
||||||
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
|
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
|
||||||
self.imgs.skull_2
|
self.imgs.skull_2
|
||||||
} else {
|
|
||||||
self.imgs.skull
|
|
||||||
})
|
|
||||||
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
|
|
||||||
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
|
|
||||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
|
|
||||||
.parent(id)
|
|
||||||
.set(state.ids.level_skull, ui);
|
|
||||||
} else {
|
|
||||||
Text::new(&format!("{}", self.stats.level.level()))
|
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
|
||||||
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
|
|
||||||
14
|
|
||||||
} else {
|
} else {
|
||||||
15
|
self.imgs.skull
|
||||||
})
|
})
|
||||||
.color(if level_comp > 4 {
|
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
|
||||||
HIGH
|
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
|
||||||
} else if level_comp < -5 {
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
|
||||||
LOW
|
|
||||||
} else {
|
|
||||||
EQUAL
|
|
||||||
})
|
|
||||||
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
|
|
||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.level, ui);
|
.set(state.ids.level_skull, ui);
|
||||||
|
} else {
|
||||||
|
Text::new(&format!("{}", self.stats.level.level()))
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
|
||||||
|
14
|
||||||
|
} else {
|
||||||
|
15
|
||||||
|
})
|
||||||
|
.color(if level_comp > 4 {
|
||||||
|
HIGH
|
||||||
|
} else if level_comp < -5 {
|
||||||
|
LOW
|
||||||
|
} else {
|
||||||
|
EQUAL
|
||||||
|
})
|
||||||
|
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
|
||||||
|
.parent(id)
|
||||||
|
.set(state.ids.level, ui);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1190,6 +1190,10 @@ impl<'a> Widget for SettingsWindow<'a> {
|
|||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(state.ids.chat_char_name_text, ui);
|
.set(state.ids.chat_char_name_text, ui);
|
||||||
|
|
||||||
|
// TODO Show account name in chat
|
||||||
|
|
||||||
|
// TODO Show account names in social window
|
||||||
|
|
||||||
// Language select drop down
|
// Language select drop down
|
||||||
Text::new(&self.localized_strings.get("common.languages"))
|
Text::new(&self.localized_strings.get("common.languages"))
|
||||||
.down_from(state.ids.chat_char_name_button, 20.0)
|
.down_from(state.ids.chat_char_name_button, 20.0)
|
||||||
|
@ -219,10 +219,14 @@ impl<'a> Widget for Skillbar<'a> {
|
|||||||
|
|
||||||
let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64);
|
let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64);
|
||||||
|
|
||||||
let hp_percentage =
|
let mut hp_percentage =
|
||||||
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
||||||
let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
|
let mut energy_percentage =
|
||||||
|
self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
|
||||||
|
if self.stats.is_dead {
|
||||||
|
hp_percentage = 0.0;
|
||||||
|
energy_percentage = 0.0;
|
||||||
|
};
|
||||||
let scale = 2.0;
|
let scale = 2.0;
|
||||||
|
|
||||||
let bar_values = self.global_state.settings.gameplay.bar_numbers;
|
let bar_values = self.global_state.settings.gameplay.bar_numbers;
|
||||||
@ -1153,15 +1157,14 @@ impl<'a> Widget for Skillbar<'a> {
|
|||||||
.w_h(100.0 * scale, 20.0 * scale)
|
.w_h(100.0 * scale, 20.0 * scale)
|
||||||
.top_left_with_margins_on(state.ids.m1_slot, 0.0, -100.0 * scale)
|
.top_left_with_margins_on(state.ids.m1_slot, 0.0, -100.0 * scale)
|
||||||
.set(state.ids.healthbar_bg, ui);
|
.set(state.ids.healthbar_bg, ui);
|
||||||
|
let health_col = match hp_percentage as u8 {
|
||||||
|
0..=20 => crit_hp_color,
|
||||||
|
21..=40 => LOW_HP_COLOR,
|
||||||
|
_ => HP_COLOR,
|
||||||
|
};
|
||||||
Image::new(self.imgs.bar_content)
|
Image::new(self.imgs.bar_content)
|
||||||
.w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale)
|
.w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale)
|
||||||
.color(Some(if hp_percentage <= 20.0 {
|
.color(Some(health_col))
|
||||||
crit_hp_color
|
|
||||||
} else if hp_percentage <= 40.0 {
|
|
||||||
LOW_HP_COLOR
|
|
||||||
} else {
|
|
||||||
HP_COLOR
|
|
||||||
}))
|
|
||||||
.top_right_with_margins_on(state.ids.healthbar_bg, 2.0 * scale, 1.0 * scale)
|
.top_right_with_margins_on(state.ids.healthbar_bg, 2.0 * scale, 1.0 * scale)
|
||||||
.set(state.ids.healthbar_filling, ui);
|
.set(state.ids.healthbar_filling, ui);
|
||||||
// Energybar
|
// Energybar
|
||||||
@ -1181,11 +1184,22 @@ impl<'a> Widget for Skillbar<'a> {
|
|||||||
// Bar Text
|
// Bar Text
|
||||||
// Values
|
// Values
|
||||||
if let BarNumbers::Values = bar_values {
|
if let BarNumbers::Values = bar_values {
|
||||||
let hp_text = format!(
|
let mut hp_text = format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
(self.stats.health.current() / 10) as u32,
|
(self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for
|
||||||
|
* living players */
|
||||||
(self.stats.health.maximum() / 10) as u32
|
(self.stats.health.maximum() / 10) as u32
|
||||||
);
|
);
|
||||||
|
let mut energy_text = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.energy.current() as u32 / 10, /* TODO Fix regeneration with smaller energy
|
||||||
|
* numbers instead of dividing by 10 here */
|
||||||
|
self.energy.maximum() as u32 / 10
|
||||||
|
);
|
||||||
|
if self.stats.is_dead {
|
||||||
|
hp_text = self.localized_strings.get("hud.group.dead").to_string();
|
||||||
|
energy_text = self.localized_strings.get("hud.group.dead").to_string();
|
||||||
|
};
|
||||||
Text::new(&hp_text)
|
Text::new(&hp_text)
|
||||||
.mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale)
|
.mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale)
|
||||||
.font_size(self.fonts.cyri.scale(14))
|
.font_size(self.fonts.cyri.scale(14))
|
||||||
@ -1198,12 +1212,6 @@ impl<'a> Widget for Skillbar<'a> {
|
|||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(state.ids.health_text, ui);
|
.set(state.ids.health_text, ui);
|
||||||
let energy_text = format!(
|
|
||||||
"{}/{}",
|
|
||||||
self.energy.current() as u32 / 10, /* TODO Fix regeneration with smaller energy
|
|
||||||
* numbers instead of dividing by 10 here */
|
|
||||||
self.energy.maximum() as u32 / 10
|
|
||||||
);
|
|
||||||
Text::new(&energy_text)
|
Text::new(&energy_text)
|
||||||
.mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale)
|
.mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale)
|
||||||
.font_size(self.fonts.cyri.scale(14))
|
.font_size(self.fonts.cyri.scale(14))
|
||||||
|
@ -1,33 +1,60 @@
|
|||||||
use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN};
|
use super::{
|
||||||
|
img_ids::{Imgs, ImgsRot},
|
||||||
|
Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts};
|
use crate::{
|
||||||
|
i18n::VoxygenLocalization,
|
||||||
|
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
|
||||||
|
};
|
||||||
use client::{self, Client};
|
use client::{self, Client};
|
||||||
|
use common::{comp::group, sync::Uid};
|
||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
color,
|
color,
|
||||||
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
||||||
widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
|
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||||
};
|
};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
widget_ids! {
|
widget_ids! {
|
||||||
pub struct Ids {
|
pub struct Ids {
|
||||||
social_frame,
|
|
||||||
social_close,
|
|
||||||
social_title,
|
|
||||||
frame,
|
frame,
|
||||||
align,
|
close,
|
||||||
content_align,
|
title_align,
|
||||||
online_tab,
|
title,
|
||||||
friends_tab,
|
bg,
|
||||||
faction_tab,
|
icon,
|
||||||
online_title,
|
|
||||||
online_no,
|
|
||||||
scrollbar,
|
scrollbar,
|
||||||
|
online_align,
|
||||||
|
online_tab,
|
||||||
|
names_align,
|
||||||
|
name_txt,
|
||||||
|
player_levels[],
|
||||||
|
player_names[],
|
||||||
|
player_zones[],
|
||||||
|
online_txt,
|
||||||
|
online_no,
|
||||||
|
levels_align,
|
||||||
|
level_txt,
|
||||||
|
zones_align,
|
||||||
|
zone_txt,
|
||||||
|
friends_tab,
|
||||||
|
//friends_tab_icon,
|
||||||
|
faction_tab,
|
||||||
|
//faction_tab_icon,
|
||||||
friends_test,
|
friends_test,
|
||||||
faction_test,
|
faction_test,
|
||||||
player_names[],
|
invite_button,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
ids: Ids,
|
||||||
|
// Holds the time when selection is made since this selection can be overriden
|
||||||
|
// by selecting an entity in-game
|
||||||
|
selected_uid: Option<(Uid, Instant)>,
|
||||||
|
}
|
||||||
|
|
||||||
pub enum SocialTab {
|
pub enum SocialTab {
|
||||||
Online,
|
Online,
|
||||||
Friends,
|
Friends,
|
||||||
@ -41,25 +68,35 @@ pub struct Social<'a> {
|
|||||||
imgs: &'a Imgs,
|
imgs: &'a Imgs,
|
||||||
fonts: &'a ConrodVoxygenFonts,
|
fonts: &'a ConrodVoxygenFonts,
|
||||||
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
|
selected_entity: Option<(specs::Entity, Instant)>,
|
||||||
|
rot_imgs: &'a ImgsRot,
|
||||||
|
tooltip_manager: &'a mut TooltipManager,
|
||||||
|
|
||||||
#[conrod(common_builder)]
|
#[conrod(common_builder)]
|
||||||
common: widget::CommonBuilder,
|
common: widget::CommonBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Social<'a> {
|
impl<'a> Social<'a> {
|
||||||
|
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
|
||||||
pub fn new(
|
pub fn new(
|
||||||
show: &'a Show,
|
show: &'a Show,
|
||||||
client: &'a Client,
|
client: &'a Client,
|
||||||
imgs: &'a Imgs,
|
imgs: &'a Imgs,
|
||||||
fonts: &'a ConrodVoxygenFonts,
|
fonts: &'a ConrodVoxygenFonts,
|
||||||
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
|
||||||
|
selected_entity: Option<(specs::Entity, Instant)>,
|
||||||
|
rot_imgs: &'a ImgsRot,
|
||||||
|
tooltip_manager: &'a mut TooltipManager,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
show,
|
show,
|
||||||
client,
|
client,
|
||||||
imgs,
|
imgs,
|
||||||
|
rot_imgs,
|
||||||
fonts,
|
fonts,
|
||||||
localized_strings,
|
localized_strings,
|
||||||
|
tooltip_manager,
|
||||||
|
selected_entity,
|
||||||
common: widget::CommonBuilder::default(),
|
common: widget::CommonBuilder::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,223 +104,467 @@ impl<'a> Social<'a> {
|
|||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Close,
|
Close,
|
||||||
|
Invite(Uid),
|
||||||
ChangeSocialTab(SocialTab),
|
ChangeSocialTab(SocialTab),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Social<'a> {
|
impl<'a> Widget for Social<'a> {
|
||||||
type Event = Vec<Event>;
|
type Event = Vec<Event>;
|
||||||
type State = Ids;
|
type State = State;
|
||||||
type Style = ();
|
type Style = ();
|
||||||
|
|
||||||
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Ids::new(id_gen) }
|
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
|
||||||
|
Self::State {
|
||||||
|
ids: Ids::new(id_gen),
|
||||||
|
selected_uid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_unit)] // TODO: Pending review in #587
|
#[allow(clippy::unused_unit)] // TODO: Pending review in #587
|
||||||
fn style(&self) -> Self::Style { () }
|
fn style(&self) -> Self::Style { () }
|
||||||
|
|
||||||
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
||||||
let widget::UpdateArgs {
|
let widget::UpdateArgs { state, ui, .. } = args;
|
||||||
/* id, */ state: ids,
|
|
||||||
ui,
|
|
||||||
..
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
let button_tooltip = Tooltip::new({
|
||||||
|
// Edge images [t, b, r, l]
|
||||||
|
// Corner images [tr, tl, br, bl]
|
||||||
|
let edge = &self.rot_imgs.tt_side;
|
||||||
|
let corner = &self.rot_imgs.tt_corner;
|
||||||
|
ImageFrame::new(
|
||||||
|
[edge.cw180, edge.none, edge.cw270, edge.cw90],
|
||||||
|
[corner.none, corner.cw270, corner.cw90, corner.cw180],
|
||||||
|
Color::Rgba(0.08, 0.07, 0.04, 1.0),
|
||||||
|
5.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.title_font_size(self.fonts.cyri.scale(15))
|
||||||
|
.parent(ui.window)
|
||||||
|
.desc_font_size(self.fonts.cyri.scale(12))
|
||||||
|
.title_text_color(TEXT_COLOR)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.desc_text_color(TEXT_COLOR);
|
||||||
|
|
||||||
Image::new(self.imgs.window_3)
|
// Window frame and BG
|
||||||
.top_left_with_margins_on(ui.window, 200.0, 25.0)
|
let pos = if self.show.group || self.show.group_menu {
|
||||||
|
200.0
|
||||||
|
} else {
|
||||||
|
25.0
|
||||||
|
};
|
||||||
|
// TODO: Different window visuals depending on the selected tab
|
||||||
|
let window_bg = match &self.show.social_tab {
|
||||||
|
SocialTab::Online => self.imgs.social_bg_on,
|
||||||
|
SocialTab::Friends => self.imgs.social_bg_friends,
|
||||||
|
SocialTab::Faction => self.imgs.social_bg_fact,
|
||||||
|
};
|
||||||
|
let window_frame = match &self.show.social_tab {
|
||||||
|
SocialTab::Online => self.imgs.social_frame_on,
|
||||||
|
SocialTab::Friends => self.imgs.social_frame_friends,
|
||||||
|
SocialTab::Faction => self.imgs.social_frame_fact,
|
||||||
|
};
|
||||||
|
Image::new(window_bg)
|
||||||
|
.bottom_left_with_margins_on(ui.window, 308.0, pos)
|
||||||
.color(Some(UI_MAIN))
|
.color(Some(UI_MAIN))
|
||||||
.w_h(103.0 * 4.0, 122.0 * 4.0)
|
.w_h(280.0, 460.0)
|
||||||
.set(ids.social_frame, ui);
|
.set(state.ids.bg, ui);
|
||||||
|
Image::new(window_frame)
|
||||||
|
.middle_of(state.ids.bg)
|
||||||
|
.color(Some(UI_HIGHLIGHT_0))
|
||||||
|
.w_h(280.0, 460.0)
|
||||||
|
.set(state.ids.frame, ui);
|
||||||
|
// Icon
|
||||||
|
Image::new(self.imgs.social)
|
||||||
|
.w_h(30.0, 30.0)
|
||||||
|
.top_left_with_margins_on(state.ids.frame, 6.0, 6.0)
|
||||||
|
.set(state.ids.icon, ui);
|
||||||
// X-Button
|
// X-Button
|
||||||
if Button::image(self.imgs.close_button)
|
if Button::image(self.imgs.close_button)
|
||||||
.w_h(28.0, 28.0)
|
.w_h(24.0, 25.0)
|
||||||
.hover_image(self.imgs.close_button_hover)
|
.hover_image(self.imgs.close_button_hover)
|
||||||
.press_image(self.imgs.close_button_press)
|
.press_image(self.imgs.close_button_press)
|
||||||
.top_right_with_margins_on(ids.social_frame, 0.0, 0.0)
|
.top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
|
||||||
.set(ids.social_close, ui)
|
.set(state.ids.close, ui)
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
events.push(Event::Close);
|
events.push(Event::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
|
Rectangle::fill_with([212.0, 42.0], color::TRANSPARENT)
|
||||||
|
.top_left_with_margins_on(state.ids.frame, 2.0, 44.0)
|
||||||
|
.set(state.ids.title_align, ui);
|
||||||
Text::new(&self.localized_strings.get("hud.social"))
|
Text::new(&self.localized_strings.get("hud.social"))
|
||||||
.mid_top_with_margin_on(ids.social_frame, 6.0)
|
.middle_of(state.ids.title_align)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(self.fonts.cyri.scale(14))
|
.font_size(self.fonts.cyri.scale(20))
|
||||||
.color(TEXT_COLOR)
|
.color(TEXT_COLOR)
|
||||||
.set(ids.social_title, ui);
|
.set(state.ids.title, ui);
|
||||||
|
|
||||||
// Alignment
|
// Tabs Buttons
|
||||||
Rectangle::fill_with([99.0 * 4.0, 112.0 * 4.0], color::TRANSPARENT)
|
// Online Tab Button
|
||||||
.mid_top_with_margin_on(ids.social_frame, 8.0 * 4.0)
|
if Button::image(match &self.show.social_tab {
|
||||||
.set(ids.align, ui);
|
SocialTab::Online => self.imgs.social_tab_online,
|
||||||
// Content Alignment
|
_ => self.imgs.social_tab_inact,
|
||||||
Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT)
|
|
||||||
.middle_of(ids.frame)
|
|
||||||
.scroll_kids()
|
|
||||||
.scroll_kids_vertically()
|
|
||||||
.set(ids.content_align, ui);
|
|
||||||
Scrollbar::y_axis(ids.content_align)
|
|
||||||
.thickness(5.0)
|
|
||||||
.rgba(0.33, 0.33, 0.33, 1.0)
|
|
||||||
.set(ids.scrollbar, ui);
|
|
||||||
// Frame
|
|
||||||
Image::new(self.imgs.social_frame)
|
|
||||||
.w_h(99.0 * 4.0, 100.0 * 4.0)
|
|
||||||
.mid_bottom_of(ids.align)
|
|
||||||
.color(Some(UI_MAIN))
|
|
||||||
.set(ids.frame, ui);
|
|
||||||
|
|
||||||
// Online Tab
|
|
||||||
|
|
||||||
if Button::image(if let SocialTab::Online = self.show.social_tab {
|
|
||||||
self.imgs.social_button_pressed
|
|
||||||
} else {
|
|
||||||
self.imgs.social_button
|
|
||||||
})
|
})
|
||||||
.w_h(30.0 * 4.0, 12.0 * 4.0)
|
.w_h(30.0, 44.0)
|
||||||
.hover_image(if let SocialTab::Online = self.show.social_tab {
|
.image_color(match &self.show.social_tab {
|
||||||
self.imgs.social_button_pressed
|
SocialTab::Online => UI_MAIN,
|
||||||
} else {
|
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
|
||||||
self.imgs.social_button_hover
|
|
||||||
})
|
})
|
||||||
.press_image(if let SocialTab::Online = self.show.social_tab {
|
.top_right_with_margins_on(state.ids.frame, 50.0, -27.0)
|
||||||
self.imgs.social_button_pressed
|
.set(state.ids.online_tab, ui)
|
||||||
} else {
|
|
||||||
self.imgs.social_button_press
|
|
||||||
})
|
|
||||||
.top_left_with_margins_on(ids.align, 4.0, 0.0)
|
|
||||||
.label(&self.localized_strings.get("hud.social.online"))
|
|
||||||
.label_font_size(self.fonts.cyri.scale(14))
|
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
|
||||||
.parent(ids.frame)
|
|
||||||
.color(UI_MAIN)
|
|
||||||
.label_color(TEXT_COLOR)
|
|
||||||
.set(ids.online_tab, ui)
|
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
events.push(Event::ChangeSocialTab(SocialTab::Online));
|
events.push(Event::ChangeSocialTab(SocialTab::Online));
|
||||||
}
|
}
|
||||||
|
// Friends Tab Button
|
||||||
// Contents
|
if Button::image(match &self.show.social_tab {
|
||||||
|
SocialTab::Friends => self.imgs.social_tab_act,
|
||||||
if let SocialTab::Online = self.show.social_tab {
|
_ => self.imgs.social_tab_inact,
|
||||||
// TODO Needs to be a string sent from the server
|
|
||||||
|
|
||||||
// Players list
|
|
||||||
// TODO: this list changes infrequently enough that it should not have to be
|
|
||||||
// recreated every frame
|
|
||||||
let players = self.client.player_list.values().filter(|p| p.is_online);
|
|
||||||
let count = players.clone().count();
|
|
||||||
if ids.player_names.len() < count {
|
|
||||||
ids.update(|ids| {
|
|
||||||
ids.player_names
|
|
||||||
.resize(count, &mut ui.widget_id_generator())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Text::new(
|
|
||||||
&self
|
|
||||||
.localized_strings
|
|
||||||
.get("hud.social.play_online_fmt")
|
|
||||||
.replace("{nb_player}", &format!("{:?}", count)),
|
|
||||||
)
|
|
||||||
.top_left_with_margins_on(ids.content_align, -2.0, 7.0)
|
|
||||||
.font_size(self.fonts.cyri.scale(14))
|
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
|
||||||
.color(TEXT_COLOR)
|
|
||||||
.set(ids.online_title, ui);
|
|
||||||
for (i, player_info) in players.enumerate() {
|
|
||||||
Text::new(&format!(
|
|
||||||
"[{}] {}",
|
|
||||||
player_info.player_alias,
|
|
||||||
match &player_info.character {
|
|
||||||
Some(character) => format!("{} Lvl {}", &character.name, &character.level),
|
|
||||||
None => "<None>".to_string(), // character select or spectating
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.down(3.0)
|
|
||||||
.font_size(self.fonts.cyri.scale(15))
|
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
|
||||||
.color(TEXT_COLOR)
|
|
||||||
.set(ids.player_names[i], ui);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Friends Tab
|
|
||||||
|
|
||||||
if Button::image(if let SocialTab::Friends = self.show.social_tab {
|
|
||||||
self.imgs.social_button_pressed
|
|
||||||
} else {
|
|
||||||
self.imgs.social_button
|
|
||||||
})
|
})
|
||||||
.w_h(30.0 * 4.0, 12.0 * 4.0)
|
.w_h(30.0, 44.0)
|
||||||
.hover_image(if let SocialTab::Friends = self.show.social_tab {
|
.hover_image(match &self.show.social_tab {
|
||||||
self.imgs.social_button_pressed
|
SocialTab::Friends => self.imgs.social_tab_act,
|
||||||
} else {
|
_ => self.imgs.social_tab_inact_hover,
|
||||||
self.imgs.social_button
|
|
||||||
})
|
})
|
||||||
.press_image(if let SocialTab::Friends = self.show.social_tab {
|
.press_image(match &self.show.social_tab {
|
||||||
self.imgs.social_button_pressed
|
SocialTab::Friends => self.imgs.social_tab_act,
|
||||||
} else {
|
_ => self.imgs.social_tab_inact_press,
|
||||||
self.imgs.social_button
|
|
||||||
})
|
})
|
||||||
.right_from(ids.online_tab, 0.0)
|
.down_from(state.ids.online_tab, 0.0)
|
||||||
.label(&self.localized_strings.get("hud.social.friends"))
|
.image_color(match &self.show.social_tab {
|
||||||
.label_font_size(self.fonts.cyri.scale(14))
|
SocialTab::Friends => UI_MAIN,
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
|
||||||
.parent(ids.frame)
|
})
|
||||||
.color(UI_MAIN)
|
.set(state.ids.friends_tab, ui)
|
||||||
.label_color(TEXT_COLOR_3)
|
|
||||||
.set(ids.friends_tab, ui)
|
|
||||||
.was_clicked()
|
.was_clicked()
|
||||||
{
|
{
|
||||||
events.push(Event::ChangeSocialTab(SocialTab::Friends));
|
events.push(Event::ChangeSocialTab(SocialTab::Friends));
|
||||||
}
|
}
|
||||||
|
// Faction Tab Button
|
||||||
// Contents
|
if Button::image(match &self.show.social_tab {
|
||||||
|
SocialTab::Friends => self.imgs.social_tab_act,
|
||||||
if let SocialTab::Friends = self.show.social_tab {
|
_ => self.imgs.social_tab_inact,
|
||||||
Text::new(&self.localized_strings.get("hud.social.not_yet_available"))
|
})
|
||||||
.middle_of(ids.content_align)
|
.w_h(30.0, 44.0)
|
||||||
.font_size(self.fonts.cyri.scale(18))
|
.hover_image(match &self.show.social_tab {
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
SocialTab::Faction => self.imgs.social_tab_act,
|
||||||
.color(TEXT_COLOR_3)
|
_ => self.imgs.social_tab_inact_hover,
|
||||||
.set(ids.friends_test, ui);
|
})
|
||||||
}
|
.press_image(match &self.show.social_tab {
|
||||||
|
SocialTab::Faction => self.imgs.social_tab_act,
|
||||||
// Faction Tab
|
_ => self.imgs.social_tab_inact_press,
|
||||||
let button_img = if let SocialTab::Faction = self.show.social_tab {
|
})
|
||||||
self.imgs.social_button_pressed
|
.down_from(state.ids.friends_tab, 0.0)
|
||||||
} else {
|
.image_color(match &self.show.social_tab {
|
||||||
self.imgs.social_button
|
SocialTab::Faction => UI_MAIN,
|
||||||
};
|
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
|
||||||
if Button::image(button_img)
|
})
|
||||||
.w_h(30.0 * 4.0, 12.0 * 4.0)
|
.set(state.ids.faction_tab, ui)
|
||||||
.right_from(ids.friends_tab, 0.0)
|
.was_clicked()
|
||||||
.label(&self.localized_strings.get("hud.social.faction"))
|
|
||||||
.parent(ids.frame)
|
|
||||||
.label_font_size(self.fonts.cyri.scale(14))
|
|
||||||
.label_font_id(self.fonts.cyri.conrod_id)
|
|
||||||
.color(UI_MAIN)
|
|
||||||
.label_color(TEXT_COLOR_3)
|
|
||||||
.set(ids.faction_tab, ui)
|
|
||||||
.was_clicked()
|
|
||||||
{
|
{
|
||||||
events.push(Event::ChangeSocialTab(SocialTab::Faction));
|
events.push(Event::ChangeSocialTab(SocialTab::Faction));
|
||||||
}
|
}
|
||||||
|
// Online Tab
|
||||||
// Contents
|
if let SocialTab::Online = self.show.social_tab {
|
||||||
|
// Content Alignments
|
||||||
if let SocialTab::Faction = self.show.social_tab {
|
Rectangle::fill_with([270.0, 346.0], color::TRANSPARENT)
|
||||||
Text::new(&self.localized_strings.get("hud.social.not_yet_available"))
|
.mid_top_with_margin_on(state.ids.frame, 74.0)
|
||||||
.middle_of(ids.content_align)
|
.scroll_kids_vertically()
|
||||||
.font_size(self.fonts.cyri.scale(18))
|
.set(state.ids.online_align, ui);
|
||||||
|
Rectangle::fill_with([133.0, 346.0], color::TRANSPARENT)
|
||||||
|
.top_left_with_margins_on(state.ids.online_align, 0.0, 0.0)
|
||||||
|
.crop_kids()
|
||||||
|
.set(state.ids.names_align, ui);
|
||||||
|
Rectangle::fill_with([39.0, 346.0], color::TRANSPARENT)
|
||||||
|
.right_from(state.ids.names_align, 2.0)
|
||||||
|
.crop_kids()
|
||||||
|
.set(state.ids.levels_align, ui);
|
||||||
|
Rectangle::fill_with([94.0, 346.0], color::TRANSPARENT)
|
||||||
|
.right_from(state.ids.levels_align, 2.0)
|
||||||
|
.crop_kids()
|
||||||
|
.set(state.ids.zones_align, ui);
|
||||||
|
Scrollbar::y_axis(state.ids.online_align)
|
||||||
|
.thickness(4.0)
|
||||||
|
.color(Color::Rgba(0.79, 1.09, 1.09, 0.0))
|
||||||
|
.set(state.ids.scrollbar, ui);
|
||||||
|
//
|
||||||
|
// Headlines
|
||||||
|
//
|
||||||
|
if Button::image(self.imgs.nothing)
|
||||||
|
.w_h(133.0, 18.0)
|
||||||
|
.top_left_with_margins_on(state.ids.frame, 52.0, 7.0)
|
||||||
|
.label(&self.localized_strings.get("hud.social.name"))
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(0.0))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(state.ids.name_txt, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
// Sort widgets by name alphabetically
|
||||||
|
}
|
||||||
|
if Button::image(self.imgs.nothing)
|
||||||
|
.w_h(39.0, 18.0)
|
||||||
|
.right_from(state.ids.name_txt, 2.0)
|
||||||
|
.label(&self.localized_strings.get("hud.social.level"))
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(0.0))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(state.ids.level_txt, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
// Sort widgets by level (increasing)
|
||||||
|
}
|
||||||
|
if Button::image(self.imgs.nothing)
|
||||||
|
.w_h(93.0, 18.0)
|
||||||
|
.right_from(state.ids.level_txt, 2.0)
|
||||||
|
.label(&self.localized_strings.get("hud.social.zone"))
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(0.0))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(state.ids.zone_txt, ui)
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
// Sort widgets by zone alphabetically
|
||||||
|
}
|
||||||
|
// Online Text
|
||||||
|
let players = self.client.player_list.iter().filter(|(_, p)| p.is_online);
|
||||||
|
let count = players.clone().count();
|
||||||
|
Text::new(&self.localized_strings.get("hud.social.online"))
|
||||||
|
.bottom_left_with_margins_on(state.ids.frame, 18.0, 10.0)
|
||||||
.font_id(self.fonts.cyri.conrod_id)
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
.color(TEXT_COLOR_3)
|
.font_size(self.fonts.cyri.scale(14))
|
||||||
.set(ids.faction_test, ui);
|
.color(TEXT_COLOR)
|
||||||
}
|
.set(state.ids.online_txt, ui);
|
||||||
|
Text::new(&(count - 1).to_string())
|
||||||
|
.right_from(state.ids.online_txt, 5.0)
|
||||||
|
.font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.font_size(self.fonts.cyri.scale(14))
|
||||||
|
.color(TEXT_COLOR)
|
||||||
|
.set(state.ids.online_no, ui);
|
||||||
|
// Adjust widget_id struct vec length to player count
|
||||||
|
if state.ids.player_levels.len() < count {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.player_levels
|
||||||
|
.resize(count, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.player_names.len() < count {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.player_names
|
||||||
|
.resize(count, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if state.ids.player_zones.len() < count {
|
||||||
|
state.update(|s| {
|
||||||
|
s.ids
|
||||||
|
.player_zones
|
||||||
|
.resize(count, &mut ui.widget_id_generator())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
// Create a name, level and zone row for every player in the list
|
||||||
|
// Filter out yourself from the online list
|
||||||
|
let my_uid = self.client.uid();
|
||||||
|
for (i, (&uid, player_info)) in
|
||||||
|
players.filter(|(uid, _)| Some(**uid) != my_uid).enumerate()
|
||||||
|
{
|
||||||
|
let hide_username = true;
|
||||||
|
let zone = "Wilderness"; // TODO Add real zone
|
||||||
|
let selected = state.selected_uid.map_or(false, |u| u.0 == uid);
|
||||||
|
let alias = &player_info.player_alias;
|
||||||
|
let name_text = match &player_info.character {
|
||||||
|
Some(character) => {
|
||||||
|
if Some(uid) == my_uid {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
&self.localized_strings.get("hud.common.you"),
|
||||||
|
&character.name
|
||||||
|
)
|
||||||
|
} else if hide_username {
|
||||||
|
character.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("[{}] {}", alias, &character.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => alias.clone(), // character select or spectating
|
||||||
|
};
|
||||||
|
let level = match &player_info.character {
|
||||||
|
Some(character) => format!("{} ", &character.level),
|
||||||
|
None => "".to_string(), // character select or spectating
|
||||||
|
};
|
||||||
|
let zone_name = match &player_info.character {
|
||||||
|
None => self.localized_strings.get("hud.group.in_menu").to_string(), /* character select or spectating */
|
||||||
|
_ => format!("{} ", &zone),
|
||||||
|
};
|
||||||
|
// Player name widgets
|
||||||
|
let button = Button::image(if !selected {
|
||||||
|
self.imgs.nothing
|
||||||
|
} else {
|
||||||
|
self.imgs.selection
|
||||||
|
});
|
||||||
|
let button = if i == 0 {
|
||||||
|
button.mid_top_with_margin_on(state.ids.names_align, 1.0)
|
||||||
|
} else {
|
||||||
|
button.down_from(state.ids.player_names[i - 1], 1.0)
|
||||||
|
};
|
||||||
|
button
|
||||||
|
.w_h(133.0, 20.0)
|
||||||
|
.hover_image(if selected {
|
||||||
|
self.imgs.selection
|
||||||
|
} else {
|
||||||
|
self.imgs.selection_hover
|
||||||
|
})
|
||||||
|
.press_image(if selected {
|
||||||
|
self.imgs.selection
|
||||||
|
} else {
|
||||||
|
self.imgs.selection_press
|
||||||
|
})
|
||||||
|
.label(&name_text)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.set(state.ids.player_names[i], ui);
|
||||||
|
// Player Levels
|
||||||
|
Button::image(if !selected {
|
||||||
|
self.imgs.nothing
|
||||||
|
} else {
|
||||||
|
self.imgs.selection
|
||||||
|
})
|
||||||
|
.w_h(39.0, 20.0)
|
||||||
|
.right_from(state.ids.player_names[i], 2.0)
|
||||||
|
.label(&level)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||||
|
.parent(state.ids.levels_align)
|
||||||
|
.set(state.ids.player_levels[i], ui);
|
||||||
|
// Player Zones
|
||||||
|
Button::image(if !selected {
|
||||||
|
self.imgs.nothing
|
||||||
|
} else {
|
||||||
|
self.imgs.selection
|
||||||
|
})
|
||||||
|
.w_h(94.0, 20.0)
|
||||||
|
.right_from(state.ids.player_levels[i], 2.0)
|
||||||
|
.label(&zone_name)
|
||||||
|
.label_font_size(self.fonts.cyri.scale(14))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id)
|
||||||
|
.label_color(TEXT_COLOR)
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||||
|
.parent(state.ids.zones_align)
|
||||||
|
.set(state.ids.player_zones[i], ui);
|
||||||
|
// Check for click
|
||||||
|
if ui
|
||||||
|
.widget_input(state.ids.player_names[i])
|
||||||
|
.clicks()
|
||||||
|
.left()
|
||||||
|
.next()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
state.update(|s| s.selected_uid = Some((uid, Instant::now())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite Button
|
||||||
|
let is_leader_or_not_in_group = self
|
||||||
|
.client
|
||||||
|
.group_info()
|
||||||
|
.map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid));
|
||||||
|
|
||||||
|
let current_members = self
|
||||||
|
.client
|
||||||
|
.group_members()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, role)| matches!(role, group::Role::Member))
|
||||||
|
.count()
|
||||||
|
+ 1;
|
||||||
|
let current_invites = self.client.pending_invites().len();
|
||||||
|
let max_members = self.client.max_group_size() as usize;
|
||||||
|
let group_not_full = current_members + current_invites < max_members;
|
||||||
|
let selected_to_invite = (is_leader_or_not_in_group && group_not_full)
|
||||||
|
.then(|| {
|
||||||
|
state
|
||||||
|
.selected_uid
|
||||||
|
.as_ref()
|
||||||
|
.map(|(s, _)| *s)
|
||||||
|
.filter(|selected| {
|
||||||
|
self.client
|
||||||
|
.player_list
|
||||||
|
.get(selected)
|
||||||
|
.map_or(false, |selected_player| {
|
||||||
|
selected_player.is_online && selected_player.character.is_some()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
self.selected_entity
|
||||||
|
.and_then(|s| self.client.state().read_component_copied(s.0))
|
||||||
|
})
|
||||||
|
.filter(|selected| {
|
||||||
|
// Prevent inviting entities already in the same group
|
||||||
|
!self.client.group_members().contains_key(selected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let invite_button = Button::image(self.imgs.button)
|
||||||
|
.w_h(106.0, 26.0)
|
||||||
|
.bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0)
|
||||||
|
.hover_image(if selected_to_invite.is_some() {
|
||||||
|
self.imgs.button_hover
|
||||||
|
} else {
|
||||||
|
self.imgs.button
|
||||||
|
})
|
||||||
|
.press_image(if selected_to_invite.is_some() {
|
||||||
|
self.imgs.button_press
|
||||||
|
} else {
|
||||||
|
self.imgs.button
|
||||||
|
})
|
||||||
|
.label(self.localized_strings.get("hud.group.invite"))
|
||||||
|
.label_y(conrod_core::position::Relative::Scalar(3.0))
|
||||||
|
.label_color(if selected_to_invite.is_some() {
|
||||||
|
TEXT_COLOR
|
||||||
|
} else {
|
||||||
|
TEXT_COLOR_3
|
||||||
|
})
|
||||||
|
.image_color(if selected_to_invite.is_some() {
|
||||||
|
TEXT_COLOR
|
||||||
|
} else {
|
||||||
|
TEXT_COLOR_3
|
||||||
|
})
|
||||||
|
.label_font_size(self.fonts.cyri.scale(15))
|
||||||
|
.label_font_id(self.fonts.cyri.conrod_id);
|
||||||
|
|
||||||
|
if if self.client.group_info().is_some() {
|
||||||
|
let tooltip_txt = format!(
|
||||||
|
"{}/{} {}",
|
||||||
|
current_members + current_invites,
|
||||||
|
max_members,
|
||||||
|
&self.localized_strings.get("hud.group.members")
|
||||||
|
);
|
||||||
|
invite_button
|
||||||
|
.with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip)
|
||||||
|
.set(state.ids.invite_button, ui)
|
||||||
|
} else {
|
||||||
|
invite_button.set(state.ids.invite_button, ui)
|
||||||
|
}
|
||||||
|
.was_clicked()
|
||||||
|
{
|
||||||
|
if let Some(uid) = selected_to_invite {
|
||||||
|
events.push(Event::Invite(uid));
|
||||||
|
state.update(|s| {
|
||||||
|
s.selected_uid = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // End of Online Tab
|
||||||
|
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ use anim::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
item::ItemKind, Body, CharacterState, Last, LightAnimation, LightEmitter, Loadout, Ori,
|
item::ItemKind, Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout,
|
||||||
PhysicsState, Pos, Scale, Stats, Vel,
|
Ori, PhysicsState, Pos, Scale, Stats, Vel,
|
||||||
},
|
},
|
||||||
state::{DeltaTime, State},
|
state::{DeltaTime, State},
|
||||||
states::triple_strike,
|
states::triple_strike,
|
||||||
@ -192,7 +192,6 @@ impl FigureMgr {
|
|||||||
.read_storage::<Pos>()
|
.read_storage::<Pos>()
|
||||||
.get(scene_data.player_entity)
|
.get(scene_data.player_entity)
|
||||||
.map_or(Vec3::zero(), |pos| pos.0);
|
.map_or(Vec3::zero(), |pos| pos.0);
|
||||||
|
|
||||||
for (
|
for (
|
||||||
i,
|
i,
|
||||||
(
|
(
|
||||||
@ -207,6 +206,7 @@ impl FigureMgr {
|
|||||||
physics,
|
physics,
|
||||||
stats,
|
stats,
|
||||||
loadout,
|
loadout,
|
||||||
|
item,
|
||||||
),
|
),
|
||||||
) in (
|
) in (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
@ -220,6 +220,7 @@ impl FigureMgr {
|
|||||||
&ecs.read_storage::<PhysicsState>(),
|
&ecs.read_storage::<PhysicsState>(),
|
||||||
ecs.read_storage::<Stats>().maybe(),
|
ecs.read_storage::<Stats>().maybe(),
|
||||||
ecs.read_storage::<Loadout>().maybe(),
|
ecs.read_storage::<Loadout>().maybe(),
|
||||||
|
ecs.read_storage::<Item>().maybe(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@ -519,7 +520,13 @@ impl FigureMgr {
|
|||||||
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32
|
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or(Rgba::broadcast(1.0));
|
.unwrap_or(Rgba::broadcast(1.0))
|
||||||
|
// Highlight targeted collectible entities
|
||||||
|
* if item.is_some() && scene_data.target_entity.map_or(false, |e| e == entity) {
|
||||||
|
Rgba::new(2.0, 2.0, 2.0, 1.0)
|
||||||
|
} else {
|
||||||
|
Rgba::one()
|
||||||
|
};
|
||||||
|
|
||||||
let scale = scale.map(|s| s.0).unwrap_or(1.0);
|
let scale = scale.map(|s| s.0).unwrap_or(1.0);
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ pub struct Scene {
|
|||||||
pub struct SceneData<'a> {
|
pub struct SceneData<'a> {
|
||||||
pub state: &'a State,
|
pub state: &'a State,
|
||||||
pub player_entity: specs::Entity,
|
pub player_entity: specs::Entity,
|
||||||
|
pub target_entity: Option<specs::Entity>,
|
||||||
pub loaded_distance: f32,
|
pub loaded_distance: f32,
|
||||||
pub view_distance: u32,
|
pub view_distance: u32,
|
||||||
pub tick: u64,
|
pub tick: u64,
|
||||||
|
@ -52,6 +52,8 @@ pub struct SessionState {
|
|||||||
free_look: bool,
|
free_look: bool,
|
||||||
auto_walk: bool,
|
auto_walk: bool,
|
||||||
is_aiming: bool,
|
is_aiming: bool,
|
||||||
|
target_entity: Option<specs::Entity>,
|
||||||
|
selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents an active game session (i.e., the one being played).
|
/// Represents an active game session (i.e., the one being played).
|
||||||
@ -86,6 +88,8 @@ impl SessionState {
|
|||||||
free_look: false,
|
free_look: false,
|
||||||
auto_walk: false,
|
auto_walk: false,
|
||||||
is_aiming: false,
|
is_aiming: false,
|
||||||
|
target_entity: None,
|
||||||
|
selected_entity: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,18 +212,6 @@ impl PlayState for SessionState {
|
|||||||
view_mat, cam_pos, ..
|
view_mat, cam_pos, ..
|
||||||
} = self.scene.camera().dependents();
|
} = self.scene.camera().dependents();
|
||||||
|
|
||||||
// Choose a spot above the player's head for item distance checks
|
|
||||||
let player_pos = match self
|
|
||||||
.client
|
|
||||||
.borrow()
|
|
||||||
.state()
|
|
||||||
.read_storage::<comp::Pos>()
|
|
||||||
.get(self.client.borrow().entity())
|
|
||||||
{
|
|
||||||
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
|
|
||||||
_ => cam_pos, // Should never happen, but a safe fallback
|
|
||||||
};
|
|
||||||
|
|
||||||
let (is_aiming, aim_dir_offset) = {
|
let (is_aiming, aim_dir_offset) = {
|
||||||
let client = self.client.borrow();
|
let client = self.client.borrow();
|
||||||
let is_aiming = client
|
let is_aiming = client
|
||||||
@ -243,30 +235,10 @@ impl PlayState for SessionState {
|
|||||||
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
|
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
|
||||||
|
|
||||||
// Check to see whether we're aiming at anything
|
// Check to see whether we're aiming at anything
|
||||||
let (build_pos, select_pos) = {
|
let (build_pos, select_pos, target_entity) =
|
||||||
let client = self.client.borrow();
|
under_cursor(&self.client.borrow(), cam_pos, cam_dir);
|
||||||
let terrain = client.state().terrain();
|
// Throw out distance info, it will be useful in the future
|
||||||
|
self.target_entity = target_entity.map(|x| x.0);
|
||||||
let cam_ray = terrain
|
|
||||||
.ray(cam_pos, cam_pos + cam_dir * 100.0)
|
|
||||||
.until(|block| block.is_tangible())
|
|
||||||
.cast();
|
|
||||||
|
|
||||||
let cam_dist = cam_ray.0;
|
|
||||||
|
|
||||||
match cam_ray.1 {
|
|
||||||
Ok(Some(_))
|
|
||||||
if player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
|
|
||||||
<= MAX_PICKUP_RANGE_SQR =>
|
|
||||||
{
|
|
||||||
(
|
|
||||||
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
|
|
||||||
Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
_ => (None, None),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let can_build = self
|
let can_build = self
|
||||||
.client
|
.client
|
||||||
@ -559,6 +531,24 @@ impl PlayState for SessionState {
|
|||||||
let camera = self.scene.camera_mut();
|
let camera = self.scene.camera_mut();
|
||||||
camera.next_mode(self.client.borrow().is_admin());
|
camera.next_mode(self.client.borrow().is_admin());
|
||||||
},
|
},
|
||||||
|
Event::InputUpdate(GameInput::Select, state) => {
|
||||||
|
if !state {
|
||||||
|
self.selected_entity =
|
||||||
|
self.target_entity.map(|e| (e, std::time::Instant::now()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Event::InputUpdate(GameInput::AcceptGroupInvite, true) => {
|
||||||
|
let mut client = self.client.borrow_mut();
|
||||||
|
if client.group_invite().is_some() {
|
||||||
|
client.accept_group_invite();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Event::InputUpdate(GameInput::DeclineGroupInvite, true) => {
|
||||||
|
let mut client = self.client.borrow_mut();
|
||||||
|
if client.group_invite().is_some() {
|
||||||
|
client.decline_group_invite();
|
||||||
|
}
|
||||||
|
},
|
||||||
Event::AnalogGameInput(input) => match input {
|
Event::AnalogGameInput(input) => match input {
|
||||||
AnalogGameInput::MovementX(v) => {
|
AnalogGameInput::MovementX(v) => {
|
||||||
self.key_state.analog_matrix.x = v;
|
self.key_state.analog_matrix.x = v;
|
||||||
@ -708,6 +698,8 @@ impl PlayState for SessionState {
|
|||||||
self.scene.camera().get_mode(),
|
self.scene.camera().get_mode(),
|
||||||
camera::CameraMode::FirstPerson
|
camera::CameraMode::FirstPerson
|
||||||
),
|
),
|
||||||
|
target_entity: self.target_entity,
|
||||||
|
selected_entity: self.selected_entity,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -971,6 +963,24 @@ impl PlayState for SessionState {
|
|||||||
HudEvent::CraftRecipe(r) => {
|
HudEvent::CraftRecipe(r) => {
|
||||||
self.client.borrow_mut().craft_recipe(&r);
|
self.client.borrow_mut().craft_recipe(&r);
|
||||||
},
|
},
|
||||||
|
HudEvent::InviteMember(uid) => {
|
||||||
|
self.client.borrow_mut().send_group_invite(uid);
|
||||||
|
},
|
||||||
|
HudEvent::AcceptInvite => {
|
||||||
|
self.client.borrow_mut().accept_group_invite();
|
||||||
|
},
|
||||||
|
HudEvent::DeclineInvite => {
|
||||||
|
self.client.borrow_mut().decline_group_invite();
|
||||||
|
},
|
||||||
|
HudEvent::KickMember(uid) => {
|
||||||
|
self.client.borrow_mut().kick_from_group(uid);
|
||||||
|
},
|
||||||
|
HudEvent::LeaveGroup => {
|
||||||
|
self.client.borrow_mut().leave_group();
|
||||||
|
},
|
||||||
|
HudEvent::AssignLeader(uid) => {
|
||||||
|
self.client.borrow_mut().assign_group_leader(uid);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -979,6 +989,7 @@ impl PlayState for SessionState {
|
|||||||
let scene_data = SceneData {
|
let scene_data = SceneData {
|
||||||
state: client.state(),
|
state: client.state(),
|
||||||
player_entity: client.entity(),
|
player_entity: client.entity(),
|
||||||
|
target_entity: self.target_entity,
|
||||||
loaded_distance: client.loaded_distance(),
|
loaded_distance: client.loaded_distance(),
|
||||||
view_distance: client.view_distance().unwrap_or(1),
|
view_distance: client.view_distance().unwrap_or(1),
|
||||||
tick: client.get_tick(),
|
tick: client.get_tick(),
|
||||||
@ -1033,6 +1044,7 @@ impl PlayState for SessionState {
|
|||||||
let scene_data = SceneData {
|
let scene_data = SceneData {
|
||||||
state: client.state(),
|
state: client.state(),
|
||||||
player_entity: client.entity(),
|
player_entity: client.entity(),
|
||||||
|
target_entity: self.target_entity,
|
||||||
loaded_distance: client.loaded_distance(),
|
loaded_distance: client.loaded_distance(),
|
||||||
view_distance: client.view_distance().unwrap_or(1),
|
view_distance: client.view_distance().unwrap_or(1),
|
||||||
tick: client.get_tick(),
|
tick: client.get_tick(),
|
||||||
@ -1055,3 +1067,108 @@ impl PlayState for SessionState {
|
|||||||
self.hud.render(renderer, self.scene.globals());
|
self.hud.render(renderer, self.scene.globals());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Max distance an entity can be "targeted"
|
||||||
|
const MAX_TARGET_RANGE: f32 = 30.0;
|
||||||
|
/// Calculate what the cursor is pointing at within the 3d scene
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn under_cursor(
|
||||||
|
client: &Client,
|
||||||
|
cam_pos: Vec3<f32>,
|
||||||
|
cam_dir: Vec3<f32>,
|
||||||
|
) -> (
|
||||||
|
Option<Vec3<i32>>,
|
||||||
|
Option<Vec3<i32>>,
|
||||||
|
Option<(specs::Entity, f32)>,
|
||||||
|
) {
|
||||||
|
// Choose a spot above the player's head for item distance checks
|
||||||
|
let player_entity = client.entity();
|
||||||
|
let player_pos = match client
|
||||||
|
.state()
|
||||||
|
.read_storage::<comp::Pos>()
|
||||||
|
.get(player_entity)
|
||||||
|
{
|
||||||
|
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
|
||||||
|
_ => cam_pos, // Should never happen, but a safe fallback
|
||||||
|
};
|
||||||
|
let terrain = client.state().terrain();
|
||||||
|
|
||||||
|
let cam_ray = terrain
|
||||||
|
.ray(cam_pos, cam_pos + cam_dir * 100.0)
|
||||||
|
.until(|block| block.is_tangible())
|
||||||
|
.cast();
|
||||||
|
|
||||||
|
let cam_dist = cam_ray.0;
|
||||||
|
|
||||||
|
// The ray hit something, is it within range?
|
||||||
|
let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if
|
||||||
|
player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
|
||||||
|
<= MAX_PICKUP_RANGE_SQR)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
|
||||||
|
Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
// See if ray hits entities
|
||||||
|
// Currently treated as spheres
|
||||||
|
let ecs = client.state().ecs();
|
||||||
|
// Don't cast through blocks
|
||||||
|
// Could check for intersection with entity from last frame to narrow this down
|
||||||
|
let cast_dist = if let Ok(Some(_)) = cam_ray.1 {
|
||||||
|
cam_dist.min(MAX_TARGET_RANGE)
|
||||||
|
} else {
|
||||||
|
MAX_TARGET_RANGE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Need to raycast by distance to cam
|
||||||
|
// But also filter out by distance to the player (but this only needs to be done
|
||||||
|
// on final result)
|
||||||
|
let mut nearby = (
|
||||||
|
&ecs.entities(),
|
||||||
|
&ecs.read_storage::<comp::Pos>(),
|
||||||
|
ecs.read_storage::<comp::Scale>().maybe(),
|
||||||
|
&ecs.read_storage::<comp::Body>()
|
||||||
|
)
|
||||||
|
.join()
|
||||||
|
.filter(|(e, _, _, _)| *e != player_entity)
|
||||||
|
.map(|(e, p, s, b)| {
|
||||||
|
const RADIUS_SCALE: f32 = 3.0;
|
||||||
|
let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE;
|
||||||
|
// Move position up from the feet
|
||||||
|
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
|
||||||
|
// Distance squared from camera to the entity
|
||||||
|
let dist_sqr = pos.distance_squared(cam_pos);
|
||||||
|
(e, pos, radius, dist_sqr)
|
||||||
|
})
|
||||||
|
// Roughly filter out entities farther than ray distance
|
||||||
|
.filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
|
||||||
|
// Ignore entities intersecting the camera
|
||||||
|
.filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
|
||||||
|
// Substract sphere radius from distance to the camera
|
||||||
|
.map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// Sort by distance
|
||||||
|
nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
|
||||||
|
|
||||||
|
let seg_ray = LineSegment3 {
|
||||||
|
start: cam_pos,
|
||||||
|
end: cam_pos + cam_dir * cam_dist,
|
||||||
|
};
|
||||||
|
// TODO: fuzzy borders
|
||||||
|
let target_entity = nearby
|
||||||
|
.iter()
|
||||||
|
.map(|(e, p, r, _)| (e, *p, r))
|
||||||
|
// Find first one that intersects the ray segment
|
||||||
|
.find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
|
||||||
|
.and_then(|(e, p, r)| {
|
||||||
|
let dist_to_player = p.distance(player_pos);
|
||||||
|
(dist_to_player - r < MAX_TARGET_RANGE).then_some((*e, dist_to_player))
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: consider setting build/select to None when targeting an entity
|
||||||
|
(build_pos, select_pos, target_entity)
|
||||||
|
}
|
||||||
|
@ -169,6 +169,9 @@ impl ControlSettings {
|
|||||||
GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9),
|
GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9),
|
||||||
GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q),
|
GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q),
|
||||||
GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt),
|
GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt),
|
||||||
|
GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y),
|
||||||
|
GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U),
|
||||||
|
GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,6 +237,9 @@ impl Default for ControlSettings {
|
|||||||
GameInput::Slot9,
|
GameInput::Slot9,
|
||||||
GameInput::Slot10,
|
GameInput::Slot10,
|
||||||
GameInput::SwapLoadout,
|
GameInput::SwapLoadout,
|
||||||
|
GameInput::Select,
|
||||||
|
GameInput::AcceptGroupInvite,
|
||||||
|
GameInput::DeclineGroupInvite,
|
||||||
];
|
];
|
||||||
for game_input in game_inputs {
|
for game_input in game_inputs {
|
||||||
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
|
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
|
||||||
|
@ -67,6 +67,9 @@ pub enum GameInput {
|
|||||||
FreeLook,
|
FreeLook,
|
||||||
AutoWalk,
|
AutoWalk,
|
||||||
CycleCamera,
|
CycleCamera,
|
||||||
|
Select,
|
||||||
|
AcceptGroupInvite,
|
||||||
|
DeclineGroupInvite,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameInput {
|
impl GameInput {
|
||||||
@ -123,6 +126,9 @@ impl GameInput {
|
|||||||
GameInput::Slot9 => "gameinput.slot9",
|
GameInput::Slot9 => "gameinput.slot9",
|
||||||
GameInput::Slot10 => "gameinput.slot10",
|
GameInput::Slot10 => "gameinput.slot10",
|
||||||
GameInput::SwapLoadout => "gameinput.swaploadout",
|
GameInput::SwapLoadout => "gameinput.swaploadout",
|
||||||
|
GameInput::Select => "gameinput.select",
|
||||||
|
GameInput::AcceptGroupInvite => "gameinput.acceptgroupinvite",
|
||||||
|
GameInput::DeclineGroupInvite => "gameinput.declinegroupinvite",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,7 +522,7 @@ impl Floor {
|
|||||||
"common.items.armor.shoulder.cultist_shoulder_purple",
|
"common.items.armor.shoulder.cultist_shoulder_purple",
|
||||||
),
|
),
|
||||||
8 => comp::Item::expect_from_asset(
|
8 => comp::Item::expect_from_asset(
|
||||||
"common.items.weapons.sword.greatsword_2h_fine-0",
|
"common.items.weapons.staff.cultist_staff",
|
||||||
),
|
),
|
||||||
9 => comp::Item::expect_from_asset(
|
9 => comp::Item::expect_from_asset(
|
||||||
"common.items.weapons.sword.greatsword_2h_fine-1",
|
"common.items.weapons.sword.greatsword_2h_fine-1",
|
||||||
|
Loading…
Reference in New Issue
Block a user