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:
Monty Marz 2020-08-08 10:59:08 +00:00
commit 7aebff26e0
82 changed files with 3675 additions and 778 deletions

View File

@ -4,4 +4,7 @@ rustflags = [
]
[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"

View File

@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Loading-Screen tips
- Feeding animation for some animals
- Power stat to weapons which affects weapon damage
- Add detection of entities under the cursor
- Functional group-system with exp-sharing and disabled damage to group members
### Changed
@ -80,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed window resizing on Mac OS X.
- Dehardcoded many item variants
- Tooltips avoid the mouse better and disappear when hovered
- Improved social window functions and visuals
### Removed

1
Cargo.lock generated
View File

@ -4638,6 +4638,7 @@ dependencies = [
"ron",
"serde",
"serde_json",
"slab",
"specs",
"specs-idvs",
"sum_type",

View File

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

View File

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

View File

@ -0,0 +1,12 @@
Item(
name: "Gem of lesser Protection",
description: "Surrounded by a discrete magical glow.",
kind: Armor(
(
kind: Neck("Neck1"),
stats: (
protection: Normal(0.5),
),
)
),
)

View File

@ -1,6 +1,6 @@
Item(
name: "Uneven Bow",
description: "Someone carved his initials into it.",
description: "Someone carved their initials into it.",
kind: Tool(
(
kind: Bow("ShortBow0"),

View File

@ -88,7 +88,7 @@
(0.50, "common.items.weapons.staff.starter_staff"),
(0.35, "common.items.weapons.staff.bone_staff"),
(0.15, "common.items.weapons.staff.amethyst_staff"),
(0.01, "common.items.weapons.staff.cultist_staff"),
//(0.01, "common.items.weapons.staff.cultist_staff"),
// hammers
(0.05, "common.items.weapons.hammer.starter_hammer"),
(0.05, "common.items.weapons.hammer.wood_hammer-0"),
@ -230,6 +230,8 @@
(0.6, "common.items.armor.ring.ring_0"),
// capes
(0.6, "common.items.armor.back.short_0"),
(0.7, "common.items.armor.back.short_1"),
// necks
(0.6, "common.items.armor.neck.neck_0"),
(0.4, "common.items.armor.neck.neck_1"),
]

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -68,6 +68,8 @@ VoxygenLocalization(
"common.none": "Kein",
"common.error": "Fehler",
"common.fatal_error": "Fataler Fehler",
"common.decline": "Ablehnen",
"common.you": "Ihr",
/// End Common section
// Message when connection to the server is lost
@ -306,7 +308,7 @@ magischen Gegenstände ergattern?"#,
"hud.settings.unbound": "-",
"hud.settings.reset_keybinds": "Auf Standard zurücksetzen",
"hud.social": "Sozial",
"hud.social": "Andere Spieler",
"hud.social.online": "Online",
"hud.social.friends": "Freunde",
"hud.social.not_yet_available": "Noch nicht verfügbar",
@ -314,6 +316,23 @@ magischen Gegenstände ergattern?"#,
"hud.social.play_online_fmt": "{nb_player} Spieler online",
"hud.spell": "Zauber",
"hud.social.name" : "Name",
"hud.social.level" : "Lvl",
"hud.social.zone" : "Gebiet",
"hud.group": "Gruppe",
"hud.group.invite_to_join": "{name} lädt euch in seine Gruppe ein!",
"hud.group.invite": "Einladen",
"hud.group.kick": "Kicken",
"hud.group.assign_leader": "Anführer",
"hud.group.leave": "Gruppe Verlassen",
"hud.group.dead" : "Tot",
"hud.group.out_of_range": "Außer Reichweite",
"hud.group.add_friend": "Freund hinzufügen",
"hud.group.link_group": "Gruppen verbinden",
"hud.group.in_menu": "In Menü",
"hud.group.members": "Gruppen Mitglieder",
"hud.crafting": "Herstellen",
"hud.crafting.recipes": "Rezepte",
@ -376,6 +395,9 @@ magischen Gegenstände ergattern?"#,
"gameinput.freelook": "Freie Sicht",
"gameinput.autowalk": "Automatisch Laufen",
"gameinput.dance": "Tanzen",
"gameinput.declinegroupinvite": "Ablehnen",
"gameinput.acceptgroupinvite": "Annehmen",
"gameinput.select": "Auswählen",
/// End GameInput section

View File

@ -65,11 +65,13 @@ VoxygenLocalization(
"common.create": "Create",
"common.okay": "Okay",
"common.accept": "Accept",
"common.decline": "Decline",
"common.disclaimer": "Disclaimer",
"common.cancel": "Cancel",
"common.none": "None",
"common.error": "Error",
"common.fatal_error": "Fatal Error",
"common.you": "You",
// Message when connection to the server is lost
"common.connection_lost": r#"Connection lost!
@ -306,12 +308,16 @@ magically infused items?"#,
"hud.settings.unbound": "None",
"hud.settings.reset_keybinds": "Reset to Defaults",
"hud.social": "Social",
"hud.social.online": "Online",
"hud.social": "Other Players",
"hud.social.online": "Online:",
"hud.social.friends": "Friends",
"hud.social.not_yet_available": "Not yet available",
"hud.social.faction": "Faction",
"hud.social.play_online_fmt": "{nb_player} player(s) online",
"hud.social.name": "Name",
"hud.social.level": "Level",
"hud.social.zone": "Zone",
"hud.crafting": "Crafting",
"hud.crafting.recipes": "Recipes",
@ -319,7 +325,20 @@ magically infused items?"#,
"hud.crafting.craft": "Craft",
"hud.crafting.tool_cata": "Requires:",
"hud.spell": "Spells",
"hud.group": "Group",
"hud.group.invite_to_join": "{name} invited you to their group!",
"hud.group.invite": "Invite",
"hud.group.kick": "Kick",
"hud.group.assign_leader": "Assign Leader",
"hud.group.leave": "Leave Group",
"hud.group.dead" : "Dead",
"hud.group.out_of_range": "Out of range",
"hud.group.add_friend": "Add to Friends",
"hud.group.link_group": "Link Groups",
"hud.group.in_menu": "In Menu",
"hud.group.members": "Group Members",
"hud.spell": "Spells",
"hud.free_look_indicator": "Free look active",
"hud.auto_walk_indicator": "Auto walk active",
@ -377,7 +396,10 @@ magically infused items?"#,
"gameinput.freelook": "Free Look",
"gameinput.autowalk": "Auto Walk",
"gameinput.dance": "Dance",
"gameinput.select": "Select Entity",
"gameinput.acceptgroupinvite": "Accept Group Invite",
"gameinput.declinegroupinvite": "Decline Group Invite",
/// End GameInput section
@ -436,7 +458,8 @@ Protection
"Press 'F1' to see all default keybindings.",
"You can type /say or /s to only chat with players directly around you.",
"You can type /region or /r to only chat with players a couple of hundred blocks around you.",
"To send private message type /tell followed by a player name and your message.",
"You can type /group or /g to only chat with players in your current group.",
"To send private messages type /tell followed by a player name and your message.",
"NPCs with the same level can have a different difficulty.",
"Look at the ground for food, chests and other loot!",
"Inventory filled with food? Try crafting better food from it!",
@ -447,7 +470,9 @@ Protection
"Press 'J' to dance. Party!",
"Press 'L-Shift' to open your Glider and conquer the skies.",
"Veloren is still in Pre-Alpha. We do our best to improve it every day!",
"If you want to join the Dev-Team or just have a chat with us join our Discord-Server.",
"If you want to join the Dev-Team or just have a chat with us join our Discord-Server.",
"You can toggle showing your amount of health on the healthbar in the settings.",
"In order to see your stats click the 'Stats' button in your inventory.",
],
"npc.speech.villager_under_attack": [
"Help, I'm under attack!",

View File

@ -1016,6 +1016,10 @@
Armor(Back("Short0")): VoxTrans(
"voxel.armor.back.short-0",
(0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.0,
),
Armor(Back("Short1")): VoxTrans(
"voxel.armor.back.short-1",
(0.0, -2.0, 0.0), (-90.0, 180.0, 0.0), 1.0,
),
Armor(Back("Admin")): VoxTrans(
"voxel.armor.back.admin",
@ -1033,6 +1037,9 @@
Armor(Neck("Neck0")): Png(
"element.icons.neck-0",
),
Armor(Neck("Neck1")): Png(
"element.icons.neck-1",
),
// Tabards
Armor(Tabard("Admin")): Png(
"element.icons.tabard_admin",

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

Binary file not shown.

View File

@ -15,6 +15,10 @@
"DungPurp0": (
vox_spec: ("armor.back.dung_purp-0", (-5.0, -1.0, -14.0)),
color: None
),
),
"Short1": (
vox_spec: ("armor.back.short-1", (-5.0, -1.0, -11.0)),
color: None
),
},
))

Binary file not shown.

Binary file not shown.

View File

@ -17,13 +17,13 @@ use byteorder::{ByteOrder, LittleEndian};
use common::{
character::CharacterItem,
comp::{
self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip,
InventoryUpdateEvent,
self, group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
InventoryManip, InventoryUpdateEvent,
},
msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification,
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg,
MAX_BYTES_CHAT_MSG,
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, InviteAnswer,
Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo,
ServerMsg, MAX_BYTES_CHAT_MSG,
},
recipe::RecipeBook,
state::State,
@ -79,6 +79,15 @@ pub struct Client {
recipe_book: RecipeBook,
available_recipes: HashSet<String>,
max_group_size: u32,
// Client has received an invite (inviter uid, time out instant)
group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>,
group_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
// Pending invites that this client has sent out
pending_invites: HashSet<Uid>,
_network: Network,
participant: Option<Participant>,
singleton_stream: Stream,
@ -126,47 +135,49 @@ impl Client {
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
// Wait for initial sync
let (state, entity, server_info, world_map, recipe_book) = block_on(async {
loop {
match stream.recv().await? {
ServerMsg::InitialSync {
entity_package,
server_info,
time_of_day,
world_map: (map_size, world_map),
recipe_book,
} => {
// TODO: Display that versions don't match in Voxygen
if &server_info.git_hash != *common::util::GIT_HASH {
warn!(
"Server is running {}[{}], you are running {}[{}], versions might \
be incompatible!",
server_info.git_hash,
server_info.git_date,
common::util::GIT_HASH.to_string(),
common::util::GIT_DATE.to_string(),
);
}
let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(
async {
loop {
match stream.recv().await? {
ServerMsg::InitialSync {
entity_package,
server_info,
time_of_day,
max_group_size,
world_map: (map_size, world_map),
recipe_book,
} => {
// TODO: Display that versions don't match in Voxygen
if &server_info.git_hash != *common::util::GIT_HASH {
warn!(
"Server is running {}[{}], you are running {}[{}], versions \
might be incompatible!",
server_info.git_hash,
server_info.git_date,
common::util::GIT_HASH.to_string(),
common::util::GIT_DATE.to_string(),
);
}
debug!("Auth Server: {:?}", server_info.auth_provider);
debug!("Auth Server: {:?}", server_info.auth_provider);
// Initialize `State`
let mut state = State::default();
// Client-only components
state
.ecs_mut()
.register::<comp::Last<comp::CharacterState>>();
// Initialize `State`
let mut state = State::default();
// Client-only components
state
.ecs_mut()
.register::<comp::Last<comp::CharacterState>>();
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize);
let mut world_map_raw =
vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/];
LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
debug!("Preparing image...");
let world_map = Arc::new(
image::DynamicImage::ImageRgba8({
assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize);
let mut world_map_raw =
vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/];
LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
debug!("Preparing image...");
let world_map = Arc::new(
image::DynamicImage::ImageRgba8({
// Should not fail if the dimensions are correct.
let world_map =
image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw);
@ -175,24 +186,26 @@ impl Client {
// Flip the image, since Voxygen uses an orientation where rotation from
// positive x axis to positive y axis is counterclockwise around the z axis.
.flipv(),
);
debug!("Done preparing image...");
);
debug!("Done preparing image...");
break Ok((
state,
entity,
server_info,
(world_map, map_size),
recipe_book,
));
},
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
err => {
warn!("whoops, server mad {:?}, ignoring", err);
},
break Ok((
state,
entity,
server_info,
(world_map, map_size),
recipe_book,
max_group_size,
));
},
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
err => {
warn!("whoops, server mad {:?}, ignoring", err);
},
}
}
}
})?;
},
)?;
stream.send(ClientMsg::Ping)?;
@ -213,6 +226,12 @@ impl Client {
recipe_book,
available_recipes: HashSet::default(),
max_group_size,
group_invite: None,
group_leader: None,
group_members: HashMap::new(),
pending_invites: HashSet::new(),
_network: network,
participant: Some(participant),
singleton_stream: stream,
@ -375,7 +394,7 @@ impl Client {
}
pub fn pick_up(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() {
if let Some(uid) = self.state.read_component_copied(entity) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Pickup(uid),
@ -424,6 +443,72 @@ impl Client {
.unwrap();
}
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {
self.group_invite
}
pub fn group_info(&self) -> Option<(String, Uid)> {
self.group_leader.map(|l| ("Group".into(), l)) // TODO
}
pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
pub fn send_group_invite(&mut self, invitee: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Invite(invitee),
)))
.unwrap()
}
pub fn accept_group_invite(&mut self) {
// Clear invite
self.group_invite.take();
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Accept,
)))
.unwrap();
}
pub fn decline_group_invite(&mut self) {
// Clear invite
self.group_invite.take();
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Decline,
)))
.unwrap();
}
pub fn leave_group(&mut self) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Leave,
)))
.unwrap();
}
pub fn kick_from_group(&mut self, uid: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Kick(uid),
)))
.unwrap();
}
pub fn assign_group_leader(&mut self, uid: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::AssignLeader(uid),
)))
.unwrap();
}
pub fn is_mounted(&self) -> bool {
self.state
.ecs()
@ -433,7 +518,7 @@ impl Client {
}
pub fn mount(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() {
if let Some(uid) = self.state.read_component_copied(entity) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::Mount(uid)))
.unwrap();
@ -690,6 +775,13 @@ impl Client {
frontend_events.append(&mut self.handle_new_messages()?);
// 3) Update client local data
// Check if the group invite has timed out and remove if so
if self
.group_invite
.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur)
{
self.group_invite = None;
}
// 4) Tick the client's LocalState
self.state.tick(dt, add_foreign_systems, true);
@ -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 => {
self.singleton_stream.send(ClientMsg::Pong)?;
},
@ -976,7 +1163,7 @@ impl Client {
self.state.ecs_mut().apply_entity_package(entity_package);
},
ServerMsg::DeleteEntity(entity) => {
if self.state.read_component_cloned::<Uid>(self.entity) != Some(entity) {
if self.uid() != Some(entity) {
self.state
.ecs_mut()
.delete_entity_and_clear_from_uid_allocator(entity.0);
@ -1086,6 +1273,9 @@ impl Client {
/// Get the player's entity.
pub fn entity(&self) -> EcsEntity { self.entity }
/// Get the player's Uid.
pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity) }
/// Get the client state
pub fn get_client_state(&self) -> ClientState { self.client_state }
@ -1137,7 +1327,7 @@ impl Client {
pub fn is_admin(&self) -> bool {
let client_uid = self
.state
.read_component_cloned::<Uid>(self.entity)
.read_component_copied::<Uid>(self.entity)
.expect("Client doesn't have a Uid!!!");
self.player_list
@ -1148,8 +1338,7 @@ impl Client {
/// Clean client ECS state
fn clean_state(&mut self) {
let client_uid = self
.state
.read_component_cloned::<Uid>(self.entity)
.uid()
.map(|u| u.into())
.expect("Client doesn't have a Uid!!!");
@ -1220,7 +1409,7 @@ impl Client {
comp::ChatType::Tell(from, to) => {
let from_alias = alias_of_uid(from);
let to_alias = alias_of_uid(to);
if Some(from) == self.state.ecs().read_storage::<Uid>().get(self.entity) {
if Some(*from) == self.uid() {
format!("To [{}]: {}", to_alias, message)
} else {
format!("From [{}]: {}", from_alias, message)

View File

@ -30,6 +30,7 @@ notify = "5.0.0-pre.3"
indexmap = "1.3.0"
sum_type = "0.2.0"
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" }
slab = "0.4.2"
[dev-dependencies]
criterion = "0.3"

View File

@ -50,7 +50,6 @@ pub enum ChatCommand {
Health,
Help,
JoinFaction,
JoinGroup,
Jump,
Kill,
KillNpcs,
@ -92,7 +91,6 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Health,
ChatCommand::Help,
ChatCommand::JoinFaction,
ChatCommand::JoinGroup,
ChatCommand::Jump,
ChatCommand::Kill,
ChatCommand::KillNpcs,
@ -246,11 +244,6 @@ impl ChatCommand {
"Join/leave the specified faction",
NoAdmin,
),
ChatCommand::JoinGroup => ChatCommandData::new(
vec![Any("group", Optional)],
"Join/leave the specified group",
NoAdmin,
),
ChatCommand::Jump => cmd(
vec![
Float("x", 0.0, Required),
@ -383,7 +376,6 @@ impl ChatCommand {
ChatCommand::Group => "group",
ChatCommand::Health => "health",
ChatCommand::JoinFaction => "join_faction",
ChatCommand::JoinGroup => "join_group",
ChatCommand::Help => "help",
ChatCommand::Jump => "jump",
ChatCommand::Kill => "kill",

View File

@ -1,10 +1,9 @@
use crate::{path::Chaser, sync::Uid};
use serde::{Deserialize, Serialize};
use specs::{Component, Entity as EcsEntity, FlaggedStorage};
use specs::{Component, Entity as EcsEntity};
use specs_idvs::IdvStorage;
use vek::*;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment {
/// Wild animals and gentle giants
Wild,
@ -52,7 +51,7 @@ impl Alignment {
}
impl Component for Alignment {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug, Default)]

View File

@ -1,4 +1,4 @@
use crate::{msg::ServerMsg, sync::Uid};
use crate::{comp::group::Group, msg::ServerMsg, sync::Uid};
use serde::{Deserialize, Serialize};
use specs::Component;
use specs_idvs::IdvStorage;
@ -15,7 +15,7 @@ pub enum ChatMode {
/// Talk to players in your region of the world
Region,
/// Talk to your current group of players
Group(String),
Group(Group),
/// Talk to your faction
Faction(String),
/// Talk to every player on the server
@ -28,16 +28,16 @@ impl Component for ChatMode {
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn new_message(&self, from: Uid, message: String) -> ChatMsg {
pub fn new_message(&self, from: Uid, message: String) -> UnresolvedChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
ChatMode::Region => ChatType::Region(from),
ChatMode::Group(name) => ChatType::Group(from, name.to_string()),
ChatMode::Faction(name) => ChatType::Faction(from, name.to_string()),
ChatMode::Group(group) => ChatType::Group(from, *group),
ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()),
ChatMode::World => ChatType::World(from),
};
ChatMsg { chat_type, message }
UnresolvedChatMsg { chat_type, message }
}
}
@ -49,7 +49,7 @@ impl Default for ChatMode {
///
/// This is a superset of `SpeechBubbleType`, which is a superset of `ChatMode`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChatType {
pub enum ChatType<G> {
/// A player came online
Online,
/// A player went offline
@ -61,7 +61,7 @@ pub enum ChatType {
/// Inform players that someone died
Kill,
/// Server notifications to a group, such as player join/leave
GroupMeta(String),
GroupMeta(G),
/// Server notifications to a faction, such as player join/leave
FactionMeta(String),
/// One-on-one chat (from, to)
@ -69,7 +69,7 @@ pub enum ChatType {
/// Chat with nearby players
Say(Uid),
/// Group chat
Group(Uid, String),
Group(Uid, G),
/// Factional chat
Faction(Uid, String),
/// Regional chat
@ -86,17 +86,18 @@ pub enum ChatType {
Loot,
}
impl ChatType {
pub fn chat_msg<S>(self, msg: S) -> ChatMsg
impl<G> ChatType<G> {
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G>
where
S: Into<String>,
{
ChatMsg {
GenericChatMsg {
chat_type: self,
message: msg.into(),
}
}
}
impl ChatType<String> {
pub fn server_msg<S>(self, msg: S) -> ServerMsg
where
S: Into<String>,
@ -106,12 +107,15 @@ impl ChatType {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMsg {
pub chat_type: ChatType,
pub struct GenericChatMsg<G> {
pub chat_type: ChatType<G>,
pub message: String,
}
impl ChatMsg {
pub type ChatMsg = GenericChatMsg<String>;
pub type UnresolvedChatMsg = GenericChatMsg<Group>;
impl<G> GenericChatMsg<G> {
pub const NPC_DISTANCE: f32 = 100.0;
pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0;
@ -121,6 +125,32 @@ impl ChatMsg {
Self { chat_type, message }
}
pub fn map_group<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> {
let chat_type = match self.chat_type {
ChatType::Online => ChatType::Online,
ChatType::Offline => ChatType::Offline,
ChatType::CommandInfo => ChatType::CommandInfo,
ChatType::CommandError => ChatType::CommandError,
ChatType::Loot => ChatType::Loot,
ChatType::FactionMeta(a) => ChatType::FactionMeta(a),
ChatType::GroupMeta(g) => ChatType::GroupMeta(f(g)),
ChatType::Kill => ChatType::Kill,
ChatType::Tell(a, b) => ChatType::Tell(a, b),
ChatType::Say(a) => ChatType::Say(a),
ChatType::Group(a, g) => ChatType::Group(a, f(g)),
ChatType::Faction(a, b) => ChatType::Faction(a, b),
ChatType::Region(a) => ChatType::Region(a),
ChatType::World(a) => ChatType::World(a),
ChatType::Npc(a, b) => ChatType::Npc(a, b),
ChatType::Meta => ChatType::Meta,
};
GenericChatMsg {
chat_type,
message: self.message,
}
}
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon();
if let ChatType::Npc(from, r) = self.chat_type {
@ -174,19 +204,6 @@ impl ChatMsg {
}
}
/// Player groups are useful when forming raiding parties and coordinating
/// gameplay.
///
/// Groups are currently just an associated String (the group's name)
#[derive(Clone, Debug)]
pub struct Group(pub String);
impl Component for Group {
type Storage = IdvStorage<Self>;
}
impl From<String> for Group {
fn from(s: String) -> Self { Group(s) }
}
/// Player factions are used to coordinate pvp vs hostile factions or segment
/// chat from the world
///

View File

@ -18,12 +18,23 @@ pub enum InventoryManip {
CraftRecipe(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum GroupManip {
Invite(Uid),
Accept,
Decline,
Leave,
Kick(Uid),
AssignLeader(Uid),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ControlEvent {
ToggleLantern,
Mount(Uid),
Unmount,
InventoryManip(InventoryManip),
GroupManip(GroupManip),
Respawn,
}

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

@ -0,0 +1,528 @@
use crate::{comp::Alignment, sync::Uid};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use slab::Slab;
use specs::{Component, FlaggedStorage, Join};
use specs_idvs::IdvStorage;
use tracing::{error, warn};
// Primitive group system
// Shortcomings include:
// - no support for more complex group structures
// - lack of npc group integration
// - relies on careful management of groups to maintain a valid state
// - the possesion rod could probably wreck this
// - clients don't know which pets are theirs (could be easy to solve by
// putting owner uid in Role::Pet)
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Group(u32);
// TODO: Hack
// Corresponds to Alignment::Enemy
pub const ENEMY: Group = Group(u32::MAX);
// Corresponds to Alignment::Npc | Alignment::Tame
pub const NPC: Group = Group(u32::MAX - 1);
impl Component for Group {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
pub struct Invite(pub specs::Entity);
impl Component for Invite {
type Storage = IdvStorage<Self>;
}
// Pending invites that an entity currently has sent out
// (invited entity, instant when invite times out)
pub struct PendingInvites(pub Vec<(specs::Entity, std::time::Instant)>);
impl Component for PendingInvites {
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug)]
pub struct GroupInfo {
// TODO: what about enemy groups, either the leader will constantly change because they have to
// be loaded or we create a dummy entity or this needs to be optional
pub leader: specs::Entity,
// Number of group members (excluding pets)
pub num_members: u32,
// Name of the group
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Role {
Member,
Pet,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChangeNotification<E> {
// :D
Added(E, Role),
// :(
Removed(E),
NewLeader(E),
// Use to put in a group overwriting existing group
NewGroup { leader: E, members: Vec<(E, Role)> },
// No longer in a group
NoGroup,
}
// Note: now that we are dipping into uids here consider just using
// ChangeNotification<Uid> everywhere
// Also note when the same notification is sent to multiple destinations the
// maping might be duplicated effort
impl<E> ChangeNotification<E> {
pub fn try_map<T>(self, f: impl Fn(E) -> Option<T>) -> Option<ChangeNotification<T>> {
match self {
Self::Added(e, r) => f(e).map(|t| ChangeNotification::Added(t, r)),
Self::Removed(e) => f(e).map(ChangeNotification::Removed),
Self::NewLeader(e) => f(e).map(ChangeNotification::NewLeader),
// Note just discards members that fail map
Self::NewGroup { leader, members } => {
f(leader).map(|leader| ChangeNotification::NewGroup {
leader,
members: members
.into_iter()
.filter_map(|(e, r)| f(e).map(|t| (t, r)))
.collect(),
})
},
Self::NoGroup => Some(ChangeNotification::NoGroup),
}
}
}
type GroupsMut<'a> = specs::WriteStorage<'a, Group>;
type Groups<'a> = specs::ReadStorage<'a, Group>;
type Alignments<'a> = specs::ReadStorage<'a, Alignment>;
type Uids<'a> = specs::ReadStorage<'a, Uid>;
#[derive(Debug, Default)]
pub struct GroupManager {
groups: Slab<GroupInfo>,
}
// Gather list of pets of the group member
// Note: iterating through all entities here could become slow at higher entity
// counts
fn pets(
entity: specs::Entity,
uid: Uid,
alignments: &Alignments,
entities: &specs::Entities,
) -> Vec<specs::Entity> {
(entities, alignments)
.join()
.filter_map(|(e, a)| {
matches!(a, Alignment::Owned(owner) if *owner == uid && e != entity).then_some(e)
})
.collect::<Vec<_>>()
}
/// Returns list of current members of a group
pub fn members<'a>(
group: Group,
groups: impl Join<Type = &'a Group> + 'a,
entities: &'a specs::Entities,
alignments: &'a Alignments,
uids: &'a Uids,
) -> impl Iterator<Item = (specs::Entity, Role)> + 'a {
(entities, groups, alignments, uids)
.join()
.filter_map(move |(e, g, a, u)| {
(*g == group).then(|| {
(
e,
if matches!(a, Alignment::Owned(owner) if owner != u) {
Role::Pet
} else {
Role::Member
},
)
})
})
}
// TODO: optimize add/remove for massive NPC groups
impl GroupManager {
pub fn group_info(&self, group: Group) -> Option<&GroupInfo> {
self.groups.get(group.0 as usize)
}
fn group_info_mut(&mut self, group: Group) -> Option<&mut GroupInfo> {
self.groups.get_mut(group.0 as usize)
}
fn create_group(&mut self, leader: specs::Entity, num_members: u32) -> Group {
Group(self.groups.insert(GroupInfo {
leader,
num_members,
name: "Group".into(),
}) as u32)
}
fn remove_group(&mut self, group: Group) { self.groups.remove(group.0 as usize); }
// Add someone to a group
// Also used to create new groups
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
pub fn add_group_member(
&mut self,
leader: specs::Entity,
new_member: specs::Entity,
entities: &specs::Entities,
groups: &mut GroupsMut,
alignments: &Alignments,
uids: &Uids,
mut notifier: impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
) {
// Ensure leader is not inviting themselves
if leader == new_member {
warn!("Attempt to form group with leader as the only member (this is disallowed)");
return;
}
// Get uid
let new_member_uid = if let Some(uid) = uids.get(new_member) {
*uid
} else {
error!("Failed to retrieve uid for the new group member");
return;
};
// If new member is a member of a different group remove that
if groups
.get(new_member)
.and_then(|g| self.group_info(*g))
.is_some()
{
self.leave_group(
new_member,
groups,
alignments,
uids,
entities,
&mut notifier,
)
}
let group = match groups.get(leader).copied() {
Some(id)
if self
.group_info(id)
.map(|info| info.leader == leader)
.unwrap_or(false) =>
{
Some(id)
},
// Member of an existing group can't be a leader
// If the lead is a member of another group leave that group first
Some(_) => {
self.leave_group(leader, groups, alignments, uids, entities, &mut notifier);
None
},
None => None,
};
let group = if let Some(group) = group {
// Increment group size
// Note: unwrap won't fail since we just retrieved the group successfully above
self.group_info_mut(group).unwrap().num_members += 1;
group
} else {
let new_group = self.create_group(leader, 2);
// Unwrap should not fail since we just found these entities and they should
// still exist Note: if there is an issue replace with a warn
groups.insert(leader, new_group).unwrap();
// Inform
notifier(leader, ChangeNotification::NewLeader(leader));
new_group
};
let new_pets = pets(new_member, new_member_uid, alignments, entities);
// Inform
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role {
Role::Member => {
notifier(e, ChangeNotification::Added(new_member, Role::Member));
notifier(new_member, ChangeNotification::Added(e, Role::Member));
new_pets.iter().for_each(|p| {
notifier(e, ChangeNotification::Added(*p, Role::Pet));
})
},
Role::Pet => {
notifier(new_member, ChangeNotification::Added(e, Role::Pet));
},
});
notifier(new_member, ChangeNotification::NewLeader(leader));
// Add group id for new member and pets
// Unwrap should not fail since we just found these entities and they should
// still exist
// Note: if there is an issue replace with a warn
let _ = groups.insert(new_member, group).unwrap();
new_pets.iter().for_each(|e| {
let _ = groups.insert(*e, group).unwrap();
});
}
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
pub fn new_pet(
&mut self,
pet: specs::Entity,
owner: specs::Entity,
groups: &mut GroupsMut,
entities: &specs::Entities,
alignments: &Alignments,
uids: &Uids,
notifier: &mut impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
) {
let group = match groups.get(owner).copied() {
Some(group) => group,
None => {
let new_group = self.create_group(owner, 1);
groups.insert(owner, new_group).unwrap();
// Inform
notifier(owner, ChangeNotification::NewLeader(owner));
new_group
},
};
// Inform
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role {
Role::Member => {
notifier(e, ChangeNotification::Added(pet, Role::Pet));
},
Role::Pet => {},
});
// Add
groups.insert(pet, group).unwrap();
}
pub fn leave_group(
&mut self,
member: specs::Entity,
groups: &mut GroupsMut,
alignments: &Alignments,
uids: &Uids,
entities: &specs::Entities,
notifier: &mut impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
) {
// Pets can't leave
if matches!(alignments.get(member), Some(Alignment::Owned(uid)) if uids.get(member).map_or(true, |u| u != uid))
{
return;
}
self.remove_from_group(member, groups, alignments, uids, entities, notifier, false);
// Set NPC back to their group
if let Some(alignment) = alignments.get(member) {
match alignment {
Alignment::Npc => {
let _ = groups.insert(member, NPC);
},
Alignment::Enemy => {
let _ = groups.insert(member, ENEMY);
},
_ => {},
}
}
}
pub fn entity_deleted(
&mut self,
member: specs::Entity,
groups: &mut GroupsMut,
alignments: &Alignments,
uids: &Uids,
entities: &specs::Entities,
notifier: &mut impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
) {
self.remove_from_group(member, groups, alignments, uids, entities, notifier, true);
}
// Remove someone from a group if they are in one
// Don't need to check if they are in a group before calling this
// Also removes pets (ie call this if the pet no longer exists)
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
fn remove_from_group(
&mut self,
member: specs::Entity,
groups: &mut GroupsMut,
alignments: &Alignments,
uids: &Uids,
entities: &specs::Entities,
notifier: &mut impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
to_be_deleted: bool,
) {
let group = match groups.get(member) {
Some(group) => *group,
None => return,
};
// If leaving entity was the leader disband the group
if self
.group_info(group)
.map(|info| info.leader == member)
.unwrap_or(false)
{
// Remove group
self.remove_group(group);
(entities, uids, &*groups, alignments.maybe())
.join()
.filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member))
.fold(
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
|mut acc, (e, uid, _, alignment)| {
if let Some(owner) = alignment.and_then(|a| match a {
Alignment::Owned(owner) if uid != owner => Some(owner),
_ => None,
}) {
// A pet
// Assumes owner will be in the group
acc.entry(*owner).or_default().1.push(e);
} else {
// Not a pet
acc.entry(*uid).or_default().0 = Some(e);
}
acc
},
)
.into_iter()
.map(|(_, v)| v)
.for_each(|(owner, pets)| {
if let Some(owner) = owner {
if !pets.is_empty() {
let mut members =
pets.iter().map(|e| (*e, Role::Pet)).collect::<Vec<_>>();
members.push((owner, Role::Member));
// New group
let new_group = self.create_group(owner, 1);
for (member, _) in &members {
groups.insert(*member, new_group).unwrap();
}
notifier(owner, ChangeNotification::NewGroup {
leader: owner,
members,
});
} else {
// If no pets just remove group
groups.remove(owner);
notifier(owner, ChangeNotification::NoGroup)
}
} else {
// Owner not found, potentially the were removed from the world
pets.into_iter().for_each(|pet| {
groups.remove(pet);
});
}
});
} else {
// Not leader
let leaving_member_uid = if let Some(uid) = uids.get(member) {
*uid
} else {
error!("Failed to retrieve uid for the leaving member");
return;
};
let leaving_pets = pets(member, leaving_member_uid, alignments, entities);
// If pets and not about to be deleted form new group
if !leaving_pets.is_empty() && !to_be_deleted {
let new_group = self.create_group(member, 1);
notifier(member, ChangeNotification::NewGroup {
leader: member,
members: leaving_pets
.iter()
.map(|p| (*p, Role::Pet))
.chain(std::iter::once((member, Role::Member)))
.collect(),
});
let _ = groups.insert(member, new_group).unwrap();
leaving_pets.iter().for_each(|&e| {
let _ = groups.insert(e, new_group).unwrap();
});
} else {
let _ = groups.remove(member);
notifier(member, ChangeNotification::NoGroup);
leaving_pets.iter().for_each(|&e| {
let _ = groups.remove(e);
});
}
if let Some(info) = self.group_info_mut(group) {
// If not pet, decrement number of members
if !matches!(alignments.get(member), Some(Alignment::Owned(owner)) if uids.get(member).map_or(true, |uid| uid != owner))
{
if info.num_members > 0 {
info.num_members -= 1;
} else {
error!("Group with invalid number of members")
}
}
let mut remaining_count = 0; // includes pets
// Inform remaining members
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| {
remaining_count += 1;
match role {
Role::Member => {
notifier(e, ChangeNotification::Removed(member));
leaving_pets.iter().for_each(|p| {
notifier(e, ChangeNotification::Removed(*p));
})
},
Role::Pet => {},
}
});
// If leader is the last one left then disband the group
// Assumes last member is the leader
if remaining_count == 1 {
let leader = info.leader;
self.remove_group(group);
groups.remove(leader);
notifier(leader, ChangeNotification::NoGroup);
} else if remaining_count == 0 {
error!("Somehow group has no members")
}
}
}
}
// Assign new group leader
// Does nothing if new leader is not part of a group
pub fn assign_leader(
&mut self,
new_leader: specs::Entity,
groups: &Groups,
entities: &specs::Entities,
alignments: &Alignments,
uids: &Uids,
mut notifier: impl FnMut(specs::Entity, ChangeNotification<specs::Entity>),
) {
let group = match groups.get(new_leader) {
Some(group) => *group,
None => return,
};
// Set new leader
self.groups[group.0 as usize].leader = new_leader;
// Point to new leader
members(group, &*groups, entities, alignments, uids).for_each(|(e, role)| match role {
Role::Member => notifier(e, ChangeNotification::NewLeader(new_leader)),
Role::Pet => {},
});
}
}

View File

@ -7,6 +7,7 @@ mod chat;
mod controller;
mod damage;
mod energy;
pub mod group;
mod inputs;
mod inventory;
mod last;
@ -28,13 +29,16 @@ pub use body::{
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
};
pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group, SpeechBubble, SpeechBubbleType};
pub use chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
};
pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip,
MountState, Mounting,
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
InventoryManip, MountState, Mounting,
};
pub use damage::{Damage, DamageSource};
pub use energy::{Energy, EnergySource};
pub use group::Group;
pub use inputs::CanBuild;
pub use inventory::{
item,

View File

@ -25,6 +25,7 @@ pub enum ServerEvent {
pos: Vec3<f32>,
power: f32,
owner: Option<Uid>,
friendly_damage: bool,
},
Damage {
uid: Uid,
@ -35,6 +36,7 @@ pub enum ServerEvent {
cause: comp::HealthSource,
},
InventoryManip(EcsEntity, comp::InventoryManip),
GroupManip(EcsEntity, comp::GroupManip),
Respawn(EcsEntity),
Shoot {
entity: EcsEntity,
@ -80,7 +82,7 @@ pub enum ServerEvent {
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),
/// Send a chat message to the player from an npc or other player
Chat(comp::ChatMsg),
Chat(comp::UnresolvedChatMsg),
}
pub struct EventBus<E> {

View File

@ -17,7 +17,7 @@ sum_type! {
LightEmitter(comp::LightEmitter),
Item(comp::Item),
Scale(comp::Scale),
Alignment(comp::Alignment),
Group(comp::Group),
MountState(comp::MountState),
Mounting(comp::Mounting),
Mass(comp::Mass),
@ -44,7 +44,7 @@ sum_type! {
LightEmitter(PhantomData<comp::LightEmitter>),
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
Alignment(PhantomData<comp::Alignment>),
Group(PhantomData<comp::Group>),
MountState(PhantomData<comp::MountState>),
Mounting(PhantomData<comp::Mounting>),
Mass(PhantomData<comp::Mass>),
@ -71,7 +71,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::MountState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world),
@ -96,7 +96,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::MountState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mounting(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world),
@ -123,7 +123,7 @@ impl sync::CompPacket for EcsCompPacket {
},
EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::Alignment(_) => sync::handle_remove::<comp::Alignment>(entity, world),
EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world),
EcsCompPhantom::MountState(_) => sync::handle_remove::<comp::MountState>(entity, world),
EcsCompPhantom::Mounting(_) => sync::handle_remove::<comp::Mounting>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),

View File

@ -7,7 +7,7 @@ pub use self::{
client::ClientMsg,
ecs_packet::EcsCompPacket,
server::{
CharacterInfo, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
CharacterInfo, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError,
RequestStateError, ServerInfo, ServerMsg,
},
};

View File

@ -47,6 +47,13 @@ pub struct CharacterInfo {
pub level: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InviteAnswer {
Accepted,
Declined,
TimedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Notification {
WaypointSaved,
@ -59,6 +66,7 @@ pub enum ServerMsg {
entity_package: sync::EntityPackage<EcsCompPacket>,
server_info: ServerInfo,
time_of_day: state::TimeOfDay,
max_group_size: u32,
world_map: (Vec2<u32>, Vec<u32>),
recipe_book: RecipeBook,
},
@ -69,6 +77,22 @@ pub enum ServerMsg {
/// An error occured while creating or deleting a character
CharacterActionError(String),
PlayerListUpdate(PlayerListUpdate),
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
// Indicate to the client that they are invited to join a group
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
},
// Indicate to the client that their sent invite was not invalid and is currently pending
InvitePending(sync::Uid),
// Note: this could potentially include all the failure cases such as inviting yourself in
// which case the `InvitePending` message could be removed and the client could consider their
// invite pending until they receive this message
// Indicate to the client the result of their invite
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
},
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
/// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state

View File

@ -123,7 +123,7 @@ impl State {
ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>();
ecs.register::<comp::Object>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Group>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();
@ -146,6 +146,7 @@ impl State {
ecs.register::<comp::Last<comp::Pos>>();
ecs.register::<comp::Last<comp::Vel>>();
ecs.register::<comp::Last<comp::Ori>>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::Agent>();
ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>();
@ -156,8 +157,9 @@ impl State {
ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
ecs.register::<comp::Group>();
ecs.register::<comp::Faction>();
ecs.register::<comp::group::Invite>();
ecs.register::<comp::group::PendingInvites>();
// Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0));
@ -168,9 +170,10 @@ impl State {
ecs.insert(TerrainGrid::new().unwrap());
ecs.insert(BlockChange::default());
ecs.insert(TerrainChanges::default());
ecs.insert(EventBus::<LocalEvent>::default());
// TODO: only register on the server
ecs.insert(EventBus::<ServerEvent>::default());
ecs.insert(EventBus::<LocalEvent>::default());
ecs.insert(comp::group::GroupManager::default());
ecs.insert(RegionMap::new());
ecs
@ -196,8 +199,8 @@ impl State {
}
/// Read a component attributed to a particular entity.
pub fn read_component_cloned<C: Component + Clone>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).cloned()
pub fn read_component_copied<C: Component + Copy>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).copied()
}
/// Get a read-only reference to the storage of a particular component type.

View File

@ -2,9 +2,12 @@ use crate::{
comp::{
self,
agent::Activity,
group,
group::Invite,
item::{tool::ToolKind, ItemKind},
Agent, Alignment, Body, CharacterState, ChatMsg, ControlAction, Controller, Loadout,
MountState, Ori, PhysicsState, Pos, Scale, Stats, Vel,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
GroupManip, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg,
Vel,
},
event::{EventBus, ServerEvent},
path::{Chaser, TraversalConfig},
@ -29,6 +32,7 @@ impl<'a> System<'a> for Sys {
Read<'a, UidAllocator>,
Read<'a, Time>,
Read<'a, DeltaTime>,
Read<'a, group::GroupManager>,
Write<'a, EventBus<ServerEvent>>,
Entities<'a>,
ReadStorage<'a, Pos>,
@ -40,12 +44,14 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, CharacterState>,
ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Uid>,
ReadStorage<'a, group::Group>,
ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>,
ReadStorage<'a, Body>,
WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>,
ReadStorage<'a, MountState>,
ReadStorage<'a, Invite>,
);
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
@ -55,6 +61,7 @@ impl<'a> System<'a> for Sys {
uid_allocator,
time,
dt,
group_manager,
event_bus,
entities,
positions,
@ -66,12 +73,14 @@ impl<'a> System<'a> for Sys {
character_states,
physics_states,
uids,
groups,
terrain,
alignments,
bodies,
mut agents,
mut controllers,
mount_states,
invites,
): Self::SystemData,
) {
for (
@ -88,6 +97,7 @@ impl<'a> System<'a> for Sys {
agent,
controller,
mount_state,
group,
) in (
&entities,
&positions,
@ -102,9 +112,23 @@ impl<'a> System<'a> for Sys {
&mut agents,
&mut controllers,
mount_states.maybe(),
groups.maybe(),
)
.join()
{
// Hack, replace with better system when groups are more sophisticated
// Override alignment if in a group unless entity is owned already
let alignment = if !matches!(alignment, Some(Alignment::Owned(_))) {
group
.and_then(|g| group_manager.group_info(*g))
.and_then(|info| uids.get(info.leader))
.copied()
.map(Alignment::Owned)
.or(alignment.copied())
} else {
alignment.copied()
};
// Skip mounted entities
if mount_state
.map(|ms| *ms != MountState::Unmounted)
@ -117,7 +141,7 @@ impl<'a> System<'a> for Sys {
let mut inputs = &mut controller.inputs;
// Default to looking in orientation direction
// Default to looking in orientation direction (can be overriden below)
inputs.look_dir = ori.0;
const AVG_FOLLOW_DIST: f32 = 6.0;
@ -148,11 +172,9 @@ impl<'a> System<'a> for Sys {
thread_rng().gen::<f32>() - 0.5,
) * 0.1
- *bearing * 0.003
- if let Some(patrol_origin) = agent.patrol_origin {
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
} else {
Vec2::zero()
};
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall
*bearing *= 0.1
@ -169,8 +191,7 @@ impl<'a> System<'a> for Sys {
.until(|block| block.is_solid())
.cast()
.1
.map(|b| b.is_none())
.unwrap_or(true)
.map_or(true, |b| b.is_none())
{
0.9
} else {
@ -269,8 +290,7 @@ impl<'a> System<'a> for Sys {
// Don't attack entities we are passive towards
// TODO: This is here, it's a bit of a hack
if let Some(alignment) = alignment {
if (*alignment).passive_towards(tgt_alignment) || tgt_stats.is_dead
{
if alignment.passive_towards(tgt_alignment) || tgt_stats.is_dead {
do_idle = true;
break 'activity;
}
@ -418,8 +438,9 @@ impl<'a> System<'a> for Sys {
if stats.get(attacker).map_or(false, |a| !a.is_dead) {
if agent.can_speak {
let msg = "npc.speech.villager_under_attack".to_string();
event_bus
.emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg)));
event_bus.emit_now(ServerEvent::Chat(
UnresolvedChatMsg::npc(*uid, msg),
));
}
agent.activity = Activity::Attack {
@ -437,7 +458,7 @@ impl<'a> System<'a> for Sys {
}
// Follow owner if we're too far, or if they're under attack
if let Some(Alignment::Owned(owner)) = alignment.copied() {
if let Some(Alignment::Owned(owner)) = alignment {
(|| {
let owner = uid_allocator.retrieve_entity_internal(owner.id())?;
@ -477,5 +498,23 @@ impl<'a> System<'a> for Sys {
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
}
// Proccess group invites
for (_invite, alignment, agent, controller) in
(&invites, &alignments, &mut agents, &mut controllers).join()
{
let accept = matches!(alignment, Alignment::Npc);
if accept {
// Clear agent comp
*agent = Agent::default();
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Accept));
} else {
controller
.events
.push(ControlEvent::GroupManip(GroupManip::Decline));
}
}
}
}

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
Alignment, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange,
HealthSource, Loadout, Ori, Pos, Scale, Stats,
group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource,
Loadout, Ori, Pos, Scale, Stats,
},
event::{EventBus, LocalEvent, ServerEvent},
sync::Uid,
@ -26,10 +26,10 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Alignment>,
ReadStorage<'a, Body>,
ReadStorage<'a, Stats>,
ReadStorage<'a, Loadout>,
ReadStorage<'a, group::Group>,
WriteStorage<'a, Attacking>,
WriteStorage<'a, CharacterState>,
);
@ -44,10 +44,10 @@ impl<'a> System<'a> for Sys {
positions,
orientations,
scales,
alignments,
bodies,
stats,
loadouts,
groups,
mut attacking_storage,
character_states,
): Self::SystemData,
@ -71,23 +71,12 @@ impl<'a> System<'a> for Sys {
attack.applied = true;
// Go through all other entities
for (
b,
uid_b,
pos_b,
ori_b,
scale_b_maybe,
alignment_b_maybe,
character_b,
stats_b,
body_b,
) in (
for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in (
&entities,
&uids,
&positions,
&orientations,
scales.maybe(),
alignments.maybe(),
character_states.maybe(),
&stats,
&bodies,
@ -111,6 +100,17 @@ impl<'a> System<'a> for Sys {
&& pos.0.distance_squared(pos_b.0) < (rad_b + scale * attack.range).powi(2)
&& ori2.angle_between(pos_b2 - pos2) < attack.max_angle + (rad_b / pos2.distance(pos_b2)).atan()
{
// See if entities are in the same group
let same_group = groups
.get(entity)
.map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(false);
// Don't heal if outside group
// Don't damage in the same group
if same_group != (attack.base_healthchange > 0) {
continue;
}
// Weapon gives base damage
let source = if attack.base_healthchange > 0 {
DamageSource::Healing
@ -121,28 +121,6 @@ impl<'a> System<'a> for Sys {
healthchange: attack.base_healthchange as f32,
source,
};
let mut knockback = attack.knockback;
// TODO: remove this, either it will remain unused or be used as a temporary
// gameplay balance
//// NPCs do less damage
//if agent_maybe.is_some() {
// healthchange = (healthchange / 1.5).min(-1.0);
//}
// TODO: remove this when there is a better way to deal with alignment
// Don't heal NPCs
if (damage.healthchange > 0.0 && alignment_b_maybe
.map(|a| !a.is_friendly_to_players())
.unwrap_or(true))
// Don't hurt pets
|| (damage.healthchange < 0.0 && alignment_b_maybe
.map(|b| Alignment::Owned(*uid).passive_towards(*b))
.unwrap_or(false))
{
damage.healthchange = 0.0;
knockback = 0.0;
}
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
&& ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
@ -160,10 +138,10 @@ impl<'a> System<'a> for Sys {
},
});
}
if knockback != 0.0 {
if attack.knockback != 0.0 {
local_emitter.emit(LocalEvent::ApplyForce {
entity: b,
force: knockback
force: attack.knockback
* *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5),
});
}

View File

@ -96,6 +96,9 @@ impl<'a> System<'a> for Sys {
}
server_emitter.emit(ServerEvent::InventoryManip(entity, manip))
},
ControlEvent::GroupManip(manip) => {
server_emitter.emit(ServerEvent::GroupManip(entity, manip))
},
ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)),
}
}

View File

@ -1,12 +1,17 @@
use crate::{
comp::{Collider, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, Scale, Sticky, Vel},
comp::{
Collider, Gravity, Group, Mass, Mounting, Ori, PhysicsState, Pos, Projectile, Scale,
Sticky, Vel,
},
event::{EventBus, ServerEvent},
state::DeltaTime,
sync::Uid,
sync::{Uid, UidAllocator},
terrain::{Block, BlockKind, TerrainGrid},
vol::ReadVol,
};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use specs::{
saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
};
use vek::*;
pub const GRAVITY: f32 = 9.81 * 5.0;
@ -44,6 +49,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Uid>,
ReadExpect<'a, TerrainGrid>,
Read<'a, DeltaTime>,
Read<'a, UidAllocator>,
Read<'a, EventBus<ServerEvent>>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Sticky>,
@ -55,6 +61,8 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
ReadStorage<'a, Mounting>,
ReadStorage<'a, Group>,
ReadStorage<'a, Projectile>,
);
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
@ -66,6 +74,7 @@ impl<'a> System<'a> for Sys {
uids,
terrain,
dt,
uid_allocator,
event_bus,
scales,
stickies,
@ -77,6 +86,8 @@ impl<'a> System<'a> for Sys {
mut velocities,
mut orientations,
mountings,
groups,
projectiles,
): Self::SystemData,
) {
let mut event_emitter = event_bus.emitter();
@ -432,7 +443,7 @@ impl<'a> System<'a> for Sys {
}
// Apply pushback
for (pos, scale, mass, vel, _, _, _, physics) in (
for (pos, scale, mass, vel, _, _, _, physics, projectile) in (
&positions,
scales.maybe(),
masses.maybe(),
@ -441,9 +452,12 @@ impl<'a> System<'a> for Sys {
!&mountings,
stickies.maybe(),
&mut physics_states,
// TODO: if we need to avoid collisions for other things consider moving whether it
// should interact into the collider component or into a separate component
projectiles.maybe(),
)
.join()
.filter(|(_, _, _, _, _, _, sticky, physics)| {
.filter(|(_, _, _, _, _, _, sticky, physics, _)| {
sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground)
})
{
@ -452,16 +466,27 @@ impl<'a> System<'a> for Sys {
let scale = scale.map(|s| s.0).unwrap_or(1.0);
let mass = mass.map(|m| m.0).unwrap_or(scale);
for (other, pos_other, scale_other, mass_other, _, _) in (
// Group to ignore collisions with
let ignore_group = projectile
.and_then(|p| p.owner)
.and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
.and_then(|e| groups.get(e));
for (other, pos_other, scale_other, mass_other, _, _, group) in (
&uids,
&positions,
scales.maybe(),
masses.maybe(),
&colliders,
!&mountings,
groups.maybe(),
)
.join()
{
if ignore_group.is_some() && ignore_group == group {
continue;
}
let scale_other = scale_other.map(|s| s.0).unwrap_or(1.0);
let mass_other = mass_other.map(|m| m.0).unwrap_or(scale_other);

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
projectile, Alignment, Damage, DamageSource, Energy, EnergySource, HealthChange,
HealthSource, Loadout, Ori, PhysicsState, Pos, Projectile, Vel,
projectile, Damage, DamageSource, Energy, EnergySource, HealthChange, HealthSource,
Loadout, Ori, PhysicsState, Pos, Projectile, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
state::DeltaTime,
@ -28,7 +28,6 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Ori>,
WriteStorage<'a, Projectile>,
WriteStorage<'a, Energy>,
ReadStorage<'a, Alignment>,
ReadStorage<'a, Loadout>,
);
@ -46,7 +45,6 @@ impl<'a> System<'a> for Sys {
mut orientations,
mut projectiles,
mut energies,
alignments,
loadouts,
): Self::SystemData,
) {
@ -72,6 +70,7 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
power,
owner: projectile.owner,
friendly_damage: false,
})
},
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
@ -92,23 +91,13 @@ impl<'a> System<'a> for Sys {
healthchange: healthchange as f32,
source: DamageSource::Projectile,
};
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
if let Some(loadout) = loadouts.get(entity) {
damage.modify_damage(false, loadout);
}
let other_entity = uid_allocator.retrieve_entity_internal(other.into());
if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) {
damage.modify_damage(false, loadout);
}
// Hacky: remove this when groups get implemented
let passive = uid_allocator
.retrieve_entity_internal(other.into())
.and_then(|other| {
alignments
.get(other)
.map(|a| Alignment::Owned(owner_uid).passive_towards(*a))
})
.unwrap_or(false);
if other != projectile.owner.unwrap() && !passive {
if other != owner_uid {
server_emitter.emit(ServerEvent::Damage {
uid: other,
change: HealthChange {
@ -143,6 +132,7 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
power,
owner: projectile.owner,
friendly_damage: false,
})
},
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {

View File

@ -2,7 +2,7 @@
//! To implement a new command, add an instance of `ChatCommand` to
//! `CHAT_COMMANDS` and provide a handler function.
use crate::{Server, StateExt};
use crate::{client::Client, Server, StateExt};
use chrono::{NaiveTime, Timelike};
use common::{
assets,
@ -77,7 +77,6 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Health => handle_health,
ChatCommand::Help => handle_help,
ChatCommand::JoinFaction => handle_join_faction,
ChatCommand::JoinGroup => handle_join_group,
ChatCommand::Jump => handle_jump,
ChatCommand::Kill => handle_kill,
ChatCommand::KillNpcs => handle_kill_npcs,
@ -227,7 +226,7 @@ fn handle_jump(
action: &ChatCommand,
) {
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
match server.state.read_component_cloned::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(current_pos) => {
server
.state
@ -252,7 +251,7 @@ fn handle_goto(
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
if server
.state
.read_component_cloned::<comp::Pos>(target)
.read_component_copied::<comp::Pos>(target)
.is_some()
{
server
@ -463,9 +462,9 @@ fn handle_tp(
);
return;
};
if let Some(_pos) = server.state.read_component_cloned::<comp::Pos>(target) {
if let Some(_pos) = server.state.read_component_copied::<comp::Pos>(target) {
if let Some(player) = opt_player {
if let Some(pos) = server.state.read_component_cloned::<comp::Pos>(player) {
if let Some(pos) = server.state.read_component_copied::<comp::Pos>(player) {
server.state.write_component(target, pos);
server.state.write_component(target, comp::ForceUpdate);
} else {
@ -510,7 +509,7 @@ fn handle_spawn(
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => {
let uid = server
.state
.read_component_cloned(target)
.read_component_copied(target)
.expect("Expected player to have a UID");
if let Some(alignment) = parse_alignment(uid, &opt_align) {
let amount = opt_amount
@ -521,7 +520,7 @@ fn handle_spawn(
let ai = opt_ai.unwrap_or_else(|| "true".to_string());
match server.state.read_component_cloned::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(pos) => {
let agent =
if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment {
@ -557,6 +556,43 @@ fn handle_spawn(
let new_entity = entity_base.build();
// Add to group system if a pet
if matches!(alignment, comp::Alignment::Owned { .. }) {
let state = server.state();
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager =
state.ecs().write_resource::<comp::group::GroupManager>();
group_manager.new_pet(
new_entity,
target,
&mut state.ecs().write_storage(),
&state.ecs().entities(),
&state.ecs().read_storage(),
&uids,
&mut |entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
} else if let Some(group) = match alignment {
comp::Alignment::Wild => None,
comp::Alignment::Enemy => Some(comp::group::ENEMY),
comp::Alignment::Npc | comp::Alignment::Tame => {
Some(comp::group::NPC)
},
comp::Alignment::Owned(_) => unreachable!(),
} {
let _ =
server.state.ecs().write_storage().insert(new_entity, group);
}
if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) {
server.notify_client(
client,
@ -594,7 +630,7 @@ fn handle_spawn_training_dummy(
_args: String,
_action: &ChatCommand,
) {
match server.state.read_component_cloned::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(pos) => {
let vel = Vec3::new(
rand::thread_rng().gen_range(-2.0, 3.0),
@ -961,13 +997,14 @@ fn handle_explosion(
let ecs = server.state.ecs();
match server.state.read_component_cloned::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(pos) => {
ecs.read_resource::<EventBus<ServerEvent>>()
.emit_now(ServerEvent::Explosion {
pos: pos.0,
power,
owner: ecs.read_storage::<Uid>().get(target).copied(),
friendly_damage: true,
})
},
None => server.notify_client(
@ -984,7 +1021,7 @@ fn handle_waypoint(
_args: String,
_action: &ChatCommand,
) {
match server.state.read_component_cloned::<comp::Pos>(target) {
match server.state.read_component_copied::<comp::Pos>(target) {
Some(pos) => {
let time = server.state.ecs().read_resource();
let _ = server
@ -1020,7 +1057,7 @@ fn handle_adminify(
Some(player) => {
let is_admin = if server
.state
.read_component_cloned::<comp::Admin>(player)
.read_component_copied::<comp::Admin>(player)
.is_some()
{
ecs.write_storage::<comp::Admin>().remove(player);
@ -1161,8 +1198,8 @@ fn handle_group(
return;
}
let ecs = server.state.ecs();
if let Some(comp::Group(group)) = ecs.read_storage().get(client) {
let mode = comp::ChatMode::Group(group.to_string());
if let Some(group) = ecs.read_storage::<comp::Group>().get(client) {
let mode = comp::ChatMode::Group(*group);
let _ = ecs.write_storage().insert(client, mode.clone());
if !msg.is_empty() {
if let Some(uid) = ecs.read_storage().get(client) {
@ -1172,7 +1209,7 @@ fn handle_group(
} else {
server.notify_client(
client,
ChatType::CommandError.server_msg("Please join a group with /join_group"),
ChatType::CommandError.server_msg("Please create a group first"),
);
}
}
@ -1323,68 +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"))]
fn handle_debug_column(
server: &mut Server,
@ -1626,7 +1601,7 @@ fn handle_remove_lights(
action: &ChatCommand,
) {
let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32);
let opt_player_pos = server.state.read_component_cloned::<comp::Pos>(target);
let opt_player_pos = server.state.read_component_copied::<comp::Pos>(target);
let mut to_delete = vec![];
match opt_player_pos {

View File

@ -1,7 +1,7 @@
use crate::{sys, Server, StateExt};
use common::{
comp::{
self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos,
self, group, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos,
Projectile, Scale, Stats, Vel, WaypointArea,
},
util::Dir,
@ -36,12 +36,26 @@ pub fn handle_create_npc(
scale: Scale,
drop_item: Option<Item>,
) {
let group = match alignment {
Alignment::Wild => None,
Alignment::Enemy => Some(group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(group::NPC),
// TODO: handle
Alignment::Owned(_) => None,
};
let entity = server
.state
.create_npc(pos, stats, loadout, body)
.with(scale)
.with(alignment);
let entity = if let Some(group) = group {
entity.with(group)
} else {
entity
};
let entity = if let Some(agent) = agent.into() {
entity.with(agent)
} else {

View File

@ -2,17 +2,17 @@ use crate::{client::Client, Server, SpawnPoint, StateExt};
use common::{
assets,
comp::{
self, item::lottery::Lottery, object, Body, Damage, DamageSource, HealthChange,
HealthSource, Player, Stats,
self, item::lottery::Lottery, object, Alignment, Body, Damage, DamageSource, Group,
HealthChange, HealthSource, Player, Pos, Stats,
},
msg::{PlayerListUpdate, ServerMsg},
state::BlockChange,
sync::{Uid, WorldSyncExt},
sync::{Uid, UidAllocator, WorldSyncExt},
sys::combat::BLOCK_ANGLE,
terrain::{Block, TerrainGrid},
vol::{ReadVol, Vox},
};
use specs::{join::Join, Entity as EcsEntity, WorldExt};
use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt};
use tracing::error;
use vek::Vec3;
@ -55,28 +55,88 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
state.notify_registered_clients(comp::ChatType::Kill.server_msg(msg));
}
{
// Give EXP to the killer if entity had stats
// Give EXP to the killer if entity had stats
(|| {
let mut stats = state.ecs().write_storage::<Stats>();
if let Some(entity_stats) = stats.get(entity).cloned() {
if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } =
cause
{
state.ecs().entity_from_uid(by.into()).map(|attacker| {
if let Some(attacker_stats) = stats.get_mut(attacker) {
// TODO: Discuss whether we should give EXP by Player
// Killing or not.
attacker_stats.exp.change_by(
(entity_stats.body_type.base_exp()
+ entity_stats.level.level()
* entity_stats.body_type.base_exp_increase())
as i64,
);
}
});
}
let by = if let HealthSource::Attack { by } | HealthSource::Projectile { owner: Some(by) } =
cause
{
by
} else {
return;
};
let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) {
attacker
} else {
return;
};
let entity_stats = if let Some(entity_stats) = stats.get(entity) {
entity_stats
} else {
return;
};
let groups = state.ecs().read_storage::<Group>();
let attacker_group = groups.get(attacker);
let destroyed_group = groups.get(entity);
// Don't give exp if attacker destroyed themselves or one of their group members
if (attacker_group.is_some() && attacker_group == destroyed_group) || attacker == entity {
return;
}
}
// Maximum distance for other group members to receive exp
const MAX_EXP_DIST: f32 = 150.0;
// Attacker gets same as exp of everyone else
const ATTACKER_EXP_WEIGHT: f32 = 1.0;
let mut exp_reward = (entity_stats.body_type.base_exp()
+ entity_stats.level.level() * entity_stats.body_type.base_exp_increase())
as f32;
// Distribute EXP to group
let positions = state.ecs().read_storage::<Pos>();
let alignments = state.ecs().read_storage::<Alignment>();
let uids = state.ecs().read_storage::<Uid>();
if let (Some(attacker_group), Some(pos)) = (attacker_group, positions.get(entity)) {
// TODO: rework if change to groups makes it easier to iterate entities in a
// group
let mut num_not_pets_in_range = 0;
let members_in_range = (
&state.ecs().entities(),
&groups,
&positions,
alignments.maybe(),
&uids,
)
.join()
.filter(|(entity, group, member_pos, _, _)| {
// Check if: in group, not main attacker, and in range
*group == attacker_group
&& *entity != attacker
&& pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2)
})
.map(|(entity, _, _, alignment, uid)| {
if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
num_not_pets_in_range += 1;
}
entity
})
.collect::<Vec<_>>();
let exp = exp_reward / (num_not_pets_in_range as f32 + ATTACKER_EXP_WEIGHT);
exp_reward = exp * ATTACKER_EXP_WEIGHT;
members_in_range.into_iter().for_each(|e| {
if let Some(stats) = stats.get_mut(e) {
stats.exp.change_by(exp.ceil() as i64);
}
});
}
if let Some(attacker_stats) = stats.get_mut(attacker) {
// TODO: Discuss whether we should give EXP by Player
// Killing or not.
attacker_stats.exp.change_by(exp_reward.ceil() as i64);
}
})();
if state
.ecs()
@ -189,7 +249,7 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
.is_some()
{
let respawn_point = state
.read_component_cloned::<comp::Waypoint>(entity)
.read_component_copied::<comp::Waypoint>(entity)
.map(|wp| wp.get_pos())
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);
@ -217,11 +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
let hit_range = 3.0 * power;
let ecs = &server.state.ecs();
for (pos_b, ori_b, character_b, stats_b, loadout_b) in (
let owner_entity = owner.and_then(|uid| {
ecs.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid.into())
});
let groups = ecs.read_storage::<comp::Group>();
for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Ori>(),
ecs.read_storage::<comp::CharacterState>().maybe(),
@ -233,9 +307,13 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, power: f32, owner: Opti
let distance_squared = pos.distance_squared(pos_b.0);
// Check if it is a hit
if !stats_b.is_dead
// Spherical wedge shaped attack field
// RADIUS
&& distance_squared < hit_range.powi(2)
// Skip if they are in the same group and friendly_damage is turned off for the
// explosion
&& (friendly_damage || !owner_entity
.and_then(|e| groups.get(e))
.map_or(false, |group_a| Some(group_a) == groups.get(entity_b)))
{
// Weapon gives base damage
let dmg = (1.0 - distance_squared / hit_range.powi(2)) * power * 130.0;

View File

@ -0,0 +1,452 @@
use crate::{client::Client, Server};
use common::{
comp::{
self,
group::{self, Group, GroupManager, Invite, PendingInvites},
ChatType, GroupManip,
},
msg::{InviteAnswer, ServerMsg},
sync,
sync::WorldSyncExt,
};
use specs::world::WorldExt;
use std::time::{Duration, Instant};
use tracing::{error, warn};
/// Time before invite times out
const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
/// Reduced duration shown to the client to help alleviate latency issues
const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
// TODO: turn chat messages into enums
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
let max_group_size = server.settings().max_player_group_size;
let state = server.state_mut();
match manip {
GroupManip::Invite(uid) => {
let mut clients = state.ecs().write_storage::<Client>();
let invitee = match state.ecs().entity_from_uid(uid.into()) {
Some(t) => t,
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta
.server_msg("Invite failed, target does not exist.".to_owned()),
);
}
return;
},
};
let uids = state.ecs().read_storage::<sync::Uid>();
// Check if entity is trying to invite themselves to a group
if uids
.get(entity)
.map_or(false, |inviter_uid| *inviter_uid == uid)
{
warn!("Entity tried to invite themselves into a group");
return;
}
// Disallow inviting entity that is already in your group
let groups = state.ecs().read_storage::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>();
let already_in_same_group = groups.get(entity).map_or(false, |group| {
group_manager
.group_info(*group)
.map_or(false, |g| g.leader == entity)
&& groups.get(invitee) == Some(group)
});
if already_in_same_group {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
"Invite failed, can't invite someone already in your group".to_owned(),
));
}
return;
}
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
// Check if group max size is already reached
// Adding the current number of pending invites
let group_size_limit_reached = state
.ecs()
.read_storage()
.get(entity)
.copied()
.and_then(|group| {
// If entity is currently the leader of a full group then they can't invite
// anyone else
group_manager
.group_info(group)
.filter(|i| i.leader == entity)
.map(|i| i.num_members)
})
.unwrap_or(1) as usize
+ pending_invites.get(entity).map_or(0, |p| p.0.len())
>= max_group_size as usize;
if group_size_limit_reached {
// Inform inviter that they have reached the group size limit
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg(
"Invite failed, pending invites plus current group size have reached \
the group size limit"
.to_owned(),
),
);
}
return;
}
let agents = state.ecs().read_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) {
// Inform inviter that there is already an invite
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta
.server_msg("This player already has a pending invite.".to_owned()),
);
}
return;
}
let mut invite_sent = false;
// Returns true if insertion was succesful
let mut send_invite = || {
match invites.insert(invitee, group::Invite(entity)) {
Err(err) => {
error!("Failed to insert Invite component: {:?}", err);
false
},
Ok(_) => {
match pending_invites.entry(entity) {
Ok(entry) => {
entry
.or_insert_with(|| PendingInvites(Vec::new()))
.0
.push((invitee, Instant::now() + INVITE_TIMEOUT_DUR));
invite_sent = true;
true
},
Err(err) => {
error!(
"Failed to get entry for pending invites component: {:?}",
err
);
// Cleanup
invites.remove(invitee);
false
},
}
},
}
};
// If client comp
if let (Some(client), Some(inviter)) =
(clients.get_mut(invitee), uids.get(entity).copied())
{
if send_invite() {
client.notify(ServerMsg::GroupInvite {
inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
});
}
} else if agents.contains(invitee) {
send_invite();
} else if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()),
);
}
// Notify inviter that the invite is pending
if invite_sent {
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::InvitePending(uid));
}
}
},
GroupManip::Accept => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
let invite_index = pending.iter().position(|p| p.0 == entity)?;
pending.swap_remove(invite_index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(inviter);
}
Some(inviter)
}) {
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::Accepted,
})
}
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
inviter,
entity,
&state.ecs().entities(),
&mut state.ecs().write_storage(),
&state.ecs().read_storage(),
&uids,
|entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
}
},
GroupManip::Decline => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
let invite_index = pending.iter().position(|p| p.0 == entity)?;
pending.swap_remove(invite_index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(inviter);
}
Some(inviter)
}) {
// Inform inviter of rejection
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::Declined,
})
}
}
},
GroupManip::Leave => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.leave_group(
entity,
&mut state.ecs().write_storage(),
&state.ecs().read_storage(),
&uids,
&state.ecs().entities(),
&mut |entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
},
GroupManip::Kick(uid) => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let alignments = state.ecs().read_storage::<comp::Alignment>();
let target = match state.ecs().entity_from_uid(uid.into()) {
Some(t) => t,
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta
.server_msg("Kick failed, target does not exist.".to_owned()),
);
}
return;
},
};
// Can't kick pet
if matches!(alignments.get(target), Some(comp::Alignment::Owned(owner)) if uids.get(target).map_or(true, |u| u != owner))
{
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg("Kick failed, you can't kick pets.".to_owned()),
);
}
return;
}
// Can't kick yourself
if uids.get(entity).map_or(false, |u| *u == uid) {
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta
.server_msg("Kick failed, you can't kick yourself.".to_owned()),
);
}
return;
}
let mut groups = state.ecs().write_storage::<group::Group>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
// Make sure kicker is the group leader
match groups
.get(target)
.and_then(|group| group_manager.group_info(*group))
{
Some(info) if info.leader == entity => {
// Remove target from group
group_manager.leave_group(
target,
&mut groups,
&state.ecs().read_storage(),
&uids,
&state.ecs().entities(),
&mut |entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
// Tell them the have been kicked
if let Some(client) = clients.get_mut(target) {
client.notify(
ChatType::Meta
.server_msg("You were removed from the group.".to_owned()),
);
}
// Tell kicker that they were succesful
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg("Player kicked.".to_owned()));
}
},
Some(_) => {
// Inform kicker that they are not the leader
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
"Kick failed: You are not the leader of the target's group.".to_owned(),
));
}
},
None => {
// Inform kicker that the target is not in a group
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg(
"Kick failed: Your target is not in a group.".to_owned(),
),
);
}
},
}
},
GroupManip::AssignLeader(uid) => {
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<sync::Uid>();
let target = match state.ecs().entity_from_uid(uid.into()) {
Some(t) => t,
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
"Leadership transfer failed, target does not exist".to_owned(),
));
}
return;
},
};
let groups = state.ecs().read_storage::<group::Group>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
// Make sure assigner is the group leader
match groups
.get(target)
.and_then(|group| group_manager.group_info(*group))
{
Some(info) if info.leader == entity => {
// Assign target as group leader
group_manager.assign_leader(
target,
&groups,
&state.ecs().entities(),
&state.ecs().read_storage(),
&uids,
|entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
// Tell them they are the leader
if let Some(client) = clients.get_mut(target) {
client.notify(
ChatType::Meta.server_msg("You are the group leader now.".to_owned()),
);
}
// Tell the old leader that the transfer was succesful
if let Some(client) = clients.get_mut(target) {
client.notify(
ChatType::Meta
.server_msg("You are no longer the group leader.".to_owned()),
);
}
},
Some(_) => {
// Inform transferer that they are not the leader
if let Some(client) = clients.get_mut(entity) {
client.notify(
ChatType::Meta.server_msg(
"Transfer failed: You are not the leader of the target's group."
.to_owned(),
),
);
}
},
None => {
// Inform transferer that the target is not in a group
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
"Transfer failed: Your target is not in a group.".to_owned(),
));
}
},
}
},
}
}

View File

@ -1,10 +1,11 @@
use crate::{Server, StateExt};
use crate::{client::Client, Server, StateExt};
use common::{
comp::{
self, item,
slot::{self, Slot},
Pos, MAX_PICKUP_RANGE_SQR,
},
msg::ServerMsg,
recipe::default_recipe_book,
sync::{Uid, WorldSyncExt},
terrain::block::Block,
@ -166,10 +167,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
thrown_items.push((
*pos,
state
.read_component_cloned::<comp::Vel>(entity)
.read_component_copied::<comp::Vel>(entity)
.unwrap_or_default(),
state
.read_component_cloned::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(entity)
.unwrap_or_default(),
*kind,
));
@ -184,7 +185,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
state.read_storage::<comp::Pos>().get(entity)
{
let uid = state
.read_component_cloned(entity)
.read_component_copied(entity)
.expect("Expected player to have a UID");
if (
&state.read_storage::<comp::Alignment>(),
@ -222,6 +223,35 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
.ecs()
.write_storage()
.insert(tameable_entity, comp::Alignment::Owned(uid));
// Add to group system
let mut clients = state.ecs().write_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager = state
.ecs()
.write_resource::<comp::group::GroupManager>(
);
group_manager.new_pet(
tameable_entity,
entity,
&mut state.ecs().write_storage(),
&state.ecs().entities(),
&state.ecs().read_storage(),
&uids,
&mut |entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| {
c.notify(ServerMsg::GroupUpdate(g))
});
},
);
let _ = state
.ecs()
.write_storage()
@ -311,7 +341,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
dropped_items.push((
*pos,
state
.read_component_cloned::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(entity)
.unwrap_or_default(),
item,
));
@ -343,10 +373,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
for _ in 0..amount {
dropped_items.push((
state
.read_component_cloned::<comp::Pos>(entity)
.read_component_copied::<comp::Pos>(entity)
.unwrap_or_default(),
state
.read_component_cloned::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(entity)
.unwrap_or_default(),
item.clone(),
));
@ -377,7 +407,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
+ Vec3::unit_z() * 15.0
+ Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0;
let uid = state.read_component_cloned::<Uid>(entity);
let uid = state.read_component_copied::<Uid>(entity);
let mut new_entity = state
.create_object(Default::default(), match kind {

View File

@ -8,6 +8,7 @@ use entity_manipulation::{
handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up,
handle_respawn,
};
use group_manip::handle_group;
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
use inventory_manip::handle_inventory;
use player::{handle_client_disconnect, handle_exit_ingame};
@ -15,6 +16,7 @@ use specs::{Entity as EcsEntity, WorldExt};
mod entity_creation;
mod entity_manipulation;
mod group_manip;
mod interaction;
mod inventory_manip;
mod player;
@ -48,9 +50,12 @@ impl Server {
for event in events {
match event {
ServerEvent::Explosion { pos, power, owner } => {
handle_explosion(&self, pos, power, owner)
},
ServerEvent::Explosion {
pos,
power,
owner,
friendly_damage,
} => handle_explosion(&self, pos, power, owner, friendly_damage),
ServerEvent::Shoot {
entity,
dir,
@ -62,6 +67,7 @@ impl Server {
ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change),
ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause),
ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip),
ServerEvent::GroupManip(entity, manip) => handle_group(self, entity, manip),
ServerEvent::Respawn(entity) => handle_respawn(&self, entity),
ServerEvent::LandOnGround { entity, vel } => {
handle_land_on_ground(&self, entity, vel)

View File

@ -4,7 +4,7 @@ use crate::{
};
use common::{
comp,
comp::Player,
comp::{group, Player},
msg::{ClientState, PlayerListUpdate, ServerMsg},
sync::{Uid, UidAllocator},
};
@ -20,8 +20,13 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
// Note: If other `ServerEvent`s are referring to this entity they will be
// disrupted
let maybe_client = state.ecs().write_storage::<Client>().remove(entity);
let maybe_uid = state.read_component_cloned::<Uid>(entity);
let maybe_uid = state.read_component_copied::<Uid>(entity);
let maybe_player = state.ecs().write_storage::<comp::Player>().remove(entity);
let maybe_group = state
.ecs()
.write_storage::<group::Group>()
.get(entity)
.cloned();
if let (Some(mut client), Some(uid), Some(player)) = (maybe_client, maybe_uid, maybe_player) {
// Tell client its request was successful
client.allow_state(ClientState::Registered);
@ -29,13 +34,39 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
client.notify(ServerMsg::ExitIngameCleanup);
let entity_builder = state.ecs_mut().create_entity().with(client).with(player);
let entity_builder = match maybe_group {
Some(group) => entity_builder.with(group),
None => entity_builder,
};
// Ensure UidAllocator maps this uid to the new entity
let uid = entity_builder
.world
.write_resource::<UidAllocator>()
.allocate(entity_builder.entity, Some(uid.into()));
entity_builder.with(uid).build();
let new_entity = entity_builder.with(uid).build();
if let Some(group) = maybe_group {
let mut group_manager = state.ecs().write_resource::<group::GroupManager>();
if group_manager
.group_info(group)
.map(|info| info.leader == entity)
.unwrap_or(false)
{
group_manager.assign_leader(
new_entity,
&state.ecs().read_storage(),
&state.ecs().entities(),
&state.ecs().read_storage(),
&state.ecs().read_storage(),
// Nothing actually changing since Uid is transferred
|_, _| {},
);
}
}
}
// Erase group component to avoid group restructure when deleting the entity
state.ecs().write_storage::<group::Group>().remove(entity);
// Delete old entity
if let Err(e) = state.delete_entity_recorded(entity) {
error!(

View File

@ -1,6 +1,6 @@
#![deny(unsafe_code)]
#![allow(clippy::option_map_unit_fn)]
#![feature(drain_filter, option_zip)]
#![feature(bool_to_option, drain_filter, option_zip)]
pub mod alias_validator;
pub mod chunk_generator;
@ -127,6 +127,7 @@ impl Server {
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default());
state.ecs_mut().insert(sys::InviteTimeoutTimer::default());
state.ecs_mut().insert(sys::PersistenceTimer::default());
// System schedulers to control execution of systems
@ -508,12 +509,18 @@ impl Server {
.nanos as i64;
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
let waypoint_nanos = self.state.ecs().read_resource::<sys::WaypointTimer>().nanos as i64;
let invite_timeout_nanos = self
.state
.ecs()
.read_resource::<sys::InviteTimeoutTimer>()
.nanos as i64;
let stats_persistence_nanos = self
.state
.ecs()
.read_resource::<sys::PersistenceTimer>()
.nanos as i64;
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos;
let total_sys_ran_in_dispatcher_nanos =
terrain_nanos + waypoint_nanos + invite_timeout_nanos;
// Report timing info
self.tick_metrics
@ -575,6 +582,10 @@ impl Server {
.tick_time
.with_label_values(&["waypoint"])
.set(waypoint_nanos);
self.tick_metrics
.tick_time
.with_label_values(&["invite timeout"])
.set(invite_timeout_nanos);
self.tick_metrics
.tick_time
.with_label_values(&["persistence:stats"])
@ -684,6 +695,7 @@ impl Server {
.create_entity_package(entity, None, None, None),
server_info: self.get_server_info(),
time_of_day: *self.state.ecs().read_resource(),
max_group_size: self.settings().max_player_group_size,
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
recipe_book: (&*default_recipe_book()).clone(),
});

View File

@ -26,6 +26,7 @@ pub struct ServerSettings {
pub persistence_db_dir: String,
pub max_view_distance: Option<u32>,
pub banned_words_files: Vec<PathBuf>,
pub max_player_group_size: u32,
}
impl Default for ServerSettings {
@ -65,6 +66,7 @@ impl Default for ServerSettings {
persistence_db_dir: "saves".to_owned(),
max_view_distance: Some(30),
banned_words_files: Vec::new(),
max_player_group_size: 6,
}
}
}

View File

@ -46,7 +46,7 @@ pub trait StateExt {
/// Performed after loading component data from the database
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
/// Iterates over registered clients and send each `ServerMsg`
fn send_chat(&self, msg: comp::ChatMsg);
fn send_chat(&self, msg: comp::UnresolvedChatMsg);
fn notify_registered_clients(&self, msg: ServerMsg);
/// Delete an entity, recording the deletion in [`DeletedEntities`]
fn delete_entity_recorded(
@ -173,7 +173,7 @@ impl StateExt for State {
self.write_component(entity, comp::CharacterState::default());
self.write_component(
entity,
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
comp::Alignment::Owned(self.read_component_copied(entity).unwrap()),
);
// Set the character id for the player
@ -213,7 +213,7 @@ impl StateExt for State {
// Notify clients of a player list update
let client_uid = self
.read_component_cloned::<Uid>(entity)
.read_component_copied::<Uid>(entity)
.map(|u| u)
.expect("Client doesn't have a Uid!!!");
@ -240,10 +240,18 @@ impl StateExt for State {
/// Send the chat message to the proper players. Say and region are limited
/// by location. Faction and group are limited by component.
fn send_chat(&self, msg: comp::ChatMsg) {
fn send_chat(&self, msg: comp::UnresolvedChatMsg) {
let ecs = self.ecs();
let is_within =
|target, a: &comp::Pos, b: &comp::Pos| a.0.distance_squared(b.0) < target * target;
let group_manager = ecs.read_resource::<comp::group::GroupManager>();
let resolved_msg = msg.clone().map_group(|group_id| {
group_manager
.group_info(group_id)
.map_or_else(|| "???".into(), |i| i.name.clone())
});
match &msg.chat_type {
comp::ChatType::Online
| comp::ChatType::Offline
@ -253,7 +261,7 @@ impl StateExt for State {
| comp::ChatType::Kill
| comp::ChatType::Meta
| comp::ChatType::World(_) => {
self.notify_registered_clients(ServerMsg::ChatMsg(msg.clone()))
self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg))
},
comp::ChatType::Tell(u, t) => {
for (client, uid) in (
@ -263,7 +271,7 @@ impl StateExt for State {
.join()
{
if uid == u || uid == t {
client.notify(ServerMsg::ChatMsg(msg.clone()));
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
},
@ -275,7 +283,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
}
@ -287,7 +295,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
}
@ -299,7 +307,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
}
@ -313,19 +321,19 @@ impl StateExt for State {
.join()
{
if s == &faction.0 {
client.notify(ServerMsg::ChatMsg(msg.clone()));
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
},
comp::ChatType::GroupMeta(s) | comp::ChatType::Group(_, s) => {
comp::ChatType::GroupMeta(g) | comp::ChatType::Group(_, g) => {
for (client, group) in (
&mut ecs.write_storage::<Client>(),
&ecs.read_storage::<comp::Group>(),
)
.join()
{
if s == &group.0 {
client.notify(ServerMsg::ChatMsg(msg.clone()));
if g == group {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
}
}
},
@ -346,6 +354,30 @@ impl StateExt for State {
&mut self,
entity: EcsEntity,
) -> Result<(), specs::error::WrongGeneration> {
// Remove entity from a group if they are in one
{
let mut clients = self.ecs().write_storage::<Client>();
let uids = self.ecs().read_storage::<Uid>();
let mut group_manager = self.ecs().write_resource::<comp::group::GroupManager>();
group_manager.entity_deleted(
entity,
&mut self.ecs().write_storage(),
&self.ecs().read_storage(),
&uids,
&self.ecs().entities(),
&mut |entity, group_change| {
clients
.get_mut(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
},
);
}
let (maybe_uid, maybe_pos) = (
self.ecs().read_storage::<Uid>().get(entity).copied(),
self.ecs().read_storage::<comp::Pos>().get(entity).copied(),

View File

@ -0,0 +1,71 @@
use super::SysTimer;
use crate::client::Client;
use common::{
comp::group::{Invite, PendingInvites},
msg::{InviteAnswer, ServerMsg},
sync::Uid,
};
use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage};
/// This system removes timed out group invites
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
type SystemData = (
Entities<'a>,
WriteStorage<'a, Invite>,
WriteStorage<'a, PendingInvites>,
WriteStorage<'a, Client>,
ReadStorage<'a, Uid>,
Write<'a, SysTimer<Self>>,
);
fn run(
&mut self,
(entities, mut invites, mut pending_invites, mut clients, uids, mut timer): Self::SystemData,
) {
timer.start();
let now = std::time::Instant::now();
let timed_out_invites = (&entities, &invites)
.join()
.filter_map(|(invitee, Invite(inviter))| {
// Retrieve timeout invite from pending invites
let pending = &mut pending_invites.get_mut(*inviter)?.0;
let index = pending.iter().position(|p| p.0 == invitee)?;
// Stop if not timed out
if pending[index].1 > now {
return None;
}
// Remove pending entry
pending.swap_remove(index);
// If no pending invites remain remove the component
if pending.is_empty() {
pending_invites.remove(*inviter);
}
// Inform inviter of timeout
if let (Some(client), Some(target)) =
(clients.get_mut(*inviter), uids.get(invitee).copied())
{
client.notify(ServerMsg::InviteComplete {
target,
answer: InviteAnswer::TimedOut,
})
}
Some(invitee)
})
.collect::<Vec<_>>();
for entity in timed_out_invites {
invites.remove(entity);
}
timer.end();
}
}

View File

@ -5,8 +5,8 @@ use crate::{
};
use common::{
comp::{
Admin, AdminList, CanBuild, ChatMode, ChatMsg, ChatType, ControlEvent, Controller,
ForceUpdate, Ori, Player, Pos, Stats, Vel,
Admin, AdminList, CanBuild, ChatMode, ChatType, ControlEvent, Controller, ForceUpdate, Ori,
Player, Pos, Stats, UnresolvedChatMsg, Vel,
},
event::{EventBus, ServerEvent},
msg::{
@ -32,7 +32,7 @@ impl Sys {
#[allow(clippy::too_many_arguments)]
async fn handle_client_msg(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, ChatMsg)>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, UnresolvedChatMsg)>,
player_list: &HashMap<Uid, PlayerInfo>,
new_players: &mut Vec<specs::Entity>,
entity: specs::Entity,
@ -202,7 +202,7 @@ impl Sys {
// Only send login message if it wasn't already
// sent previously
if !client.login_msg_sent {
new_chat_msgs.push((None, ChatMsg {
new_chat_msgs.push((None, UnresolvedChatMsg {
chat_type: ChatType::Online,
message: format!("[{}] is now online.", &player.alias), // TODO: Localize this
}));
@ -461,7 +461,7 @@ impl<'a> System<'a> for Sys {
let mut server_emitter = server_event_bus.emitter();
let mut new_chat_msgs: Vec<(Option<specs::Entity>, ChatMsg)> = Vec::new();
let mut new_chat_msgs = Vec::new();
// Player list to send new players.
let player_list = (&uids, &players, stats.maybe(), admins.maybe())

View File

@ -1,4 +1,5 @@
pub mod entity_sync;
pub mod invite_timeout;
pub mod message;
pub mod object;
pub mod persistence;
@ -21,6 +22,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type InviteTimeoutTimer = SysTimer<invite_timeout::Sys>;
pub type PersistenceTimer = SysTimer<persistence::Sys>;
pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
@ -32,12 +34,14 @@ pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "server_waypoint_sys";
const INVITE_TIMEOUT_SYS: &str = "server_invite_timeout_sys";
const PERSISTENCE_SYS: &str = "server_persistence_sys";
const OBJECT_SYS: &str = "server_object_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
dispatch_builder.add(invite_timeout::Sys, INVITE_TIMEOUT_SYS, &[]);
dispatch_builder.add(persistence::Sys, PERSISTENCE_SYS, &[]);
dispatch_builder.add(object::Sys, OBJECT_SYS, &[]);
}

View File

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

View File

@ -1,7 +1,7 @@
use super::SysTimer;
use common::{
comp::{
Alignment, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter,
Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter,
Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
},
msg::EcsCompPacket,
@ -48,7 +48,7 @@ pub struct TrackedComps<'a> {
pub scale: ReadStorage<'a, Scale>,
pub mounting: ReadStorage<'a, Mounting>,
pub mount_state: ReadStorage<'a, MountState>,
pub alignment: ReadStorage<'a, Alignment>,
pub group: ReadStorage<'a, Group>,
pub mass: ReadStorage<'a, Mass>,
pub collider: ReadStorage<'a, Collider>,
pub sticky: ReadStorage<'a, Sticky>,
@ -105,7 +105,7 @@ impl<'a> TrackedComps<'a> {
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.alignment
self.group
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
@ -151,7 +151,7 @@ pub struct ReadTrackers<'a> {
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
pub mounting: ReadExpect<'a, UpdateTracker<Mounting>>,
pub mount_state: ReadExpect<'a, UpdateTracker<MountState>>,
pub alignment: ReadExpect<'a, UpdateTracker<Alignment>>,
pub group: ReadExpect<'a, UpdateTracker<Group>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
@ -184,7 +184,7 @@ impl<'a> ReadTrackers<'a> {
.with_component(&comps.uid, &*self.scale, &comps.scale, filter)
.with_component(&comps.uid, &*self.mounting, &comps.mounting, filter)
.with_component(&comps.uid, &*self.mount_state, &comps.mount_state, filter)
.with_component(&comps.uid, &*self.alignment, &comps.alignment, filter)
.with_component(&comps.uid, &*self.group, &comps.group, filter)
.with_component(&comps.uid, &*self.mass, &comps.mass, filter)
.with_component(&comps.uid, &*self.collider, &comps.collider, filter)
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
@ -214,7 +214,7 @@ pub struct WriteTrackers<'a> {
scale: WriteExpect<'a, UpdateTracker<Scale>>,
mounting: WriteExpect<'a, UpdateTracker<Mounting>>,
mount_state: WriteExpect<'a, UpdateTracker<MountState>>,
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
group: WriteExpect<'a, UpdateTracker<Group>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>,
collider: WriteExpect<'a, UpdateTracker<Collider>>,
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
@ -236,7 +236,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers.scale.record_changes(&comps.scale);
trackers.mounting.record_changes(&comps.mounting);
trackers.mount_state.record_changes(&comps.mount_state);
trackers.alignment.record_changes(&comps.alignment);
trackers.group.record_changes(&comps.group);
trackers.mass.record_changes(&comps.mass);
trackers.collider.record_changes(&comps.collider);
trackers.sticky.record_changes(&comps.sticky);
@ -291,7 +291,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Scale>();
world.register_tracker::<Mounting>();
world.register_tracker::<MountState>();
world.register_tracker::<Alignment>();
world.register_tracker::<Group>();
world.register_tracker::<Mass>();
world.register_tracker::<Collider>();
world.register_tracker::<Sticky>();

View File

@ -1,5 +1,5 @@
use common::{
generation::{ChunkSupplement, EntityInfo, EntityKind},
generation::{ChunkSupplement, EntityInfo},
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, Vox, WriteVol},
};
@ -30,17 +30,10 @@ impl World {
let mut supplement = ChunkSupplement::default();
if chunk_pos.map(|e| e % 8 == 0).reduce_and() {
supplement = supplement.with_entity(EntityInfo {
pos: Vec3::<f32>::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0,
kind: EntityKind::Waypoint,
});
}
Ok((
TerrainChunk::new(
256 + if rng.gen::<u8>() < 64 { height } else { 0 },
Block::new(BlockKind::Dense, Rgb::new(200, 220, 255)),
Block::new(BlockKind::Grass, Rgb::new(11, 102, 35)),
Block::empty(),
TerrainChunkMeta::void(),
),

View File

@ -229,7 +229,7 @@ impl<'a> Widget for Bag<'a> {
)
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.inventory_title_bg, ui);
Text::new(
@ -240,7 +240,7 @@ impl<'a> Widget for Bag<'a> {
)
.top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(state.ids.inventory_title, ui);
// Scrollbar-BG
@ -585,7 +585,7 @@ impl<'a> Widget for Bag<'a> {
"{}\n\n{}\n\n{}\n\n{}%",
self.stats.endurance, self.stats.fitness, self.stats.willpower, damage_reduction
))
.top_right_with_margins_on(state.ids.stats_alignment, 120.0, 150.0)
.top_right_with_margins_on(state.ids.stats_alignment, 120.0, 130.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(16))
.color(TEXT_COLOR)

View File

@ -41,7 +41,7 @@ widget_ids! {
crafting_button_bg,
crafting_text,
crafting_text_bg,
group_button,
}
}
#[derive(WidgetCommon)]
@ -360,6 +360,7 @@ impl<'a> Widget for Buttons<'a> {
.color(TEXT_COLOR)
.set(state.ids.spellbook_text, ui);
}
// Crafting
if Button::image(self.imgs.crafting_icon)
.w_h(25.0, 25.0)
@ -396,6 +397,7 @@ impl<'a> Widget for Buttons<'a> {
.color(TEXT_COLOR)
.set(state.ids.crafting_text, ui);
}
None
}
}

View File

@ -475,7 +475,7 @@ fn cursor_offset_to_index(
}
/// Get the color and icon for the current line in the chat box
fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
match chat_type {
ChatType::Online => (ONLINE_COLOR, imgs.chat_online_small),
ChatType::Offline => (OFFLINE_COLOR, imgs.chat_offline_small),

View File

@ -117,23 +117,6 @@ impl<'a> Widget for Crafting<'a> {
)
});
}
/*if state.ids.recipe_img_frame.len() < self.client.recipe_book().iter().len() {
state.update(|state| {
state.ids.recipe_img_frame.resize(
self.client.recipe_book().iter().len(),
&mut ui.widget_id_generator(),
)
});
}
if state.ids.recipe_img.len() < self.client.recipe_book().iter().len() {
state.update(|state| {
state.ids.recipe_img.resize(
self.client.recipe_book().iter().len(),
&mut ui.widget_id_generator(),
)
});
}*/
let ids = &state.ids;
let mut events = Vec::new();
@ -186,7 +169,7 @@ impl<'a> Widget for Crafting<'a> {
Text::new(&self.localized_strings.get("hud.crafting"))
.mid_top_with_margin_on(ids.window_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22))
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(ids.title_main, ui);

685
voxygen/src/hud/group.rs Normal file
View 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
}
}

View File

@ -59,14 +59,19 @@ image_ids! {
selection_press: "voxygen.element.frames.selection_press",
// Social Window
social_button: "voxygen.element.buttons.social_tab",
social_button_pressed: "voxygen.element.buttons.social_tab_pressed",
social_button_hover: "voxygen.element.buttons.social_tab_hover",
social_button_press: "voxygen.element.buttons.social_tab_press",
social_frame: "voxygen.element.frames.social_frame",
social_frame_on: "voxygen.element.misc_bg.social_frame",
social_bg_on: "voxygen.element.misc_bg.social_bg",
social_frame_friends: "voxygen.element.misc_bg.social_frame",
social_bg_friends: "voxygen.element.misc_bg.social_bg",
social_frame_fact: "voxygen.element.misc_bg.social_frame",
social_bg_fact: "voxygen.element.misc_bg.social_bg",
social_tab_act: "voxygen.element.buttons.social_tab_active",
social_tab_online: "voxygen.element.misc_bg.social_tab_online",
social_tab_inact: "voxygen.element.buttons.social_tab_inactive",
social_tab_inact_hover: "voxygen.element.buttons.social_tab_inactive",
social_tab_inact_press: "voxygen.element.buttons.social_tab_inactive",
// Crafting Window
crafting_window: "voxygen.element.misc_bg.crafting",
crafting_frame: "voxygen.element.misc_bg.crafting_frame",
crafting_icon_bordered: "voxygen.element.icons.anvil",
@ -74,6 +79,9 @@ image_ids! {
crafting_icon_hover: "voxygen.element.buttons.anvil_hover",
crafting_icon_press: "voxygen.element.buttons.anvil_press",
// Group Window
member_frame: "voxygen.element.frames.group_member_frame",
member_bg: "voxygen.element.frames.group_member_bg",
// Chat-Arrows
chat_arrow: "voxygen.element.buttons.arrow_down",
@ -94,7 +102,6 @@ image_ids! {
slider_indicator_small: "voxygen.element.slider.indicator_round",
// Buttons
settings: "voxygen.element.buttons.settings",
settings_hover: "voxygen.element.buttons.settings_hover",
settings_press: "voxygen.element.buttons.settings_press",
@ -111,6 +118,10 @@ image_ids! {
spellbook_hover: "voxygen.element.buttons.spellbook_hover",
spellbook_press: "voxygen.element.buttons.spellbook_press",
group_icon: "voxygen.element.buttons.group",
group_icon_hover: "voxygen.element.buttons.group_hover",
group_icon_press: "voxygen.element.buttons.group_press",
// Skill Icons
twohsword_m1: "voxygen.element.icons.2hsword_m1",
twohsword_m2: "voxygen.element.icons.2hsword_m2",

View File

@ -3,6 +3,7 @@ mod buttons;
mod chat;
mod crafting;
mod esc_menu;
mod group;
mod hotbar;
mod img_ids;
mod item_imgs;
@ -30,6 +31,7 @@ use chat::Chat;
use chrono::NaiveTime;
use crafting::Crafting;
use esc_menu::EscMenu;
use group::Group;
use img_ids::Imgs;
use item_imgs::ItemImgs;
use map::Map;
@ -69,7 +71,7 @@ const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0);
const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0);
const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
//const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5);
const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5);
const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
@ -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 CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0);
//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
@ -109,12 +112,18 @@ const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0);
/// Color for collected loot messages
const LOOT_COLOR: Color = Color::Rgba(0.69, 0.57, 1.0, 1.0);
//Nametags
const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
const DEFAULT_NPC: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// UI Color-Theme
const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
//const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark
const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);
//const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0);
/// Distance at which nametags are visible for group members
const NAMETAG_GROUP_RANGE: f32 = 300.0;
/// Distance at which nametags are visible
const NAMETAG_RANGE: f32 = 40.0;
/// Time nametags stay visible after doing damage even if they are out of range
@ -208,6 +217,7 @@ widget_ids! {
social_window,
crafting_window,
settings_window,
group_window,
// Free look indicator
free_look_txt,
@ -242,6 +252,8 @@ pub struct DebugInfo {
pub struct HudInfo {
pub is_aiming: bool,
pub is_first_person: bool,
pub target_entity: Option<specs::Entity>,
pub selected_entity: Option<(specs::Entity, std::time::Instant)>,
}
pub enum Event {
@ -297,6 +309,12 @@ pub enum Event {
ChangeAutoWalkBehavior(PressBehavior),
ChangeStopAutoWalkOnInput(bool),
CraftRecipe(String),
InviteMember(common::sync::Uid),
AcceptInvite,
DeclineInvite,
KickMember(common::sync::Uid),
LeaveGroup,
AssignLeader(common::sync::Uid),
}
// TODO: Are these the possible layouts we want?
@ -352,6 +370,8 @@ pub struct Show {
bag: bool,
social: bool,
spell: bool,
group: bool,
group_menu: bool,
esc_menu: bool,
open_windows: Windows,
map: bool,
@ -389,7 +409,6 @@ impl Show {
fn social(&mut self, open: bool) {
if !self.esc_menu {
self.social = open;
self.crafting = false;
self.spell = false;
self.want_grab = !open;
}
@ -489,7 +508,7 @@ impl Show {
}
fn toggle_social(&mut self) {
self.social = !self.social;
self.social(!self.social);
self.spell = false;
}
@ -598,6 +617,8 @@ impl Hud {
ui: true,
social: false,
spell: false,
group: false,
group_menu: false,
mini_map: true,
settings_tab: SettingsTab::Interface,
social_tab: SocialTab::Online,
@ -1032,7 +1053,7 @@ impl Hud {
}
// Render overhead name tags and health bars
for (pos, name, stats, energy, height_offset, hpfl, uid) in (
for (pos, name, stats, energy, height_offset, hpfl, uid, in_group) in (
&entities,
&pos,
interpolated.maybe(),
@ -1045,11 +1066,34 @@ impl Hud {
&uids,
)
.join()
.filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
// Don't show outside a certain range
.filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| {
pos.0.distance_squared(player_pos)
< (if hpfl
.map(|(a, b, c, d, e, f, g, h, i, uid)| {
(
a,
b,
c,
d,
e,
f,
g,
h,
i,
uid,
client.group_members().contains_key(uid),
)
})
.filter(|(entity, pos, _, stats, _, _, _, _, hpfl, _, in_group)| {
*entity != me && !stats.is_dead
&& (stats.health.current() != stats.health.maximum()
|| info.target_entity.map_or(false, |e| e == *entity)
|| info.selected_entity.map_or(false, |s| s.0 == *entity)
|| *in_group
)
// Don't show outside a certain range
&& pos.0.distance_squared(player_pos)
< (if *in_group
{
NAMETAG_GROUP_RANGE
} else if hpfl
.time_since_last_dmg_by_me
.map_or(false, |t| t < NAMETAG_DMG_TIME)
{
@ -1059,25 +1103,40 @@ impl Hud {
})
.powi(2)
})
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, uid)| {
// TODO: This is temporary
// If the player used the default character name display their name instead
let name = if stats.name == "Character Name" {
player.map_or(&stats.name, |p| &p.alias)
} else {
&stats.name
};
(
interpolated.map_or(pos.0, |i| i.pos),
name,
.map(
|(
_,
pos,
interpolated,
stats,
energy,
// TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
player,
scale,
body,
hpfl,
uid,
)
})
in_group,
)| {
// TODO: This is temporary
// If the player used the default character name display their name instead
let name = if stats.name == "Character Name" {
player.map_or(&stats.name, |p| &p.alias)
} else {
&stats.name
};
(
interpolated.map_or(pos.0, |i| i.pos),
name,
stats,
energy,
// TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
hpfl,
uid,
in_group,
)
},
)
{
let bubble = self.speech_bubbles.get(uid);
@ -1094,6 +1153,7 @@ impl Hud {
stats,
energy,
own_level,
in_group,
&global_state.settings.gameplay,
self.pulse,
&self.voxygen_i18n,
@ -1863,23 +1923,56 @@ impl Hud {
// Social Window
if self.show.social {
for event in Social::new(
&self.show,
client,
&self.imgs,
&self.fonts,
&self.voxygen_i18n,
)
.set(self.ids.social_window, ui_widgets)
{
match event {
social::Event::Close => self.show.social(false),
social::Event::ChangeSocialTab(social_tab) => {
self.show.open_social_tab(social_tab)
},
let ecs = client.state().ecs();
let _stats = ecs.read_storage::<comp::Stats>();
let me = client.entity();
if let Some(_stats) = stats.get(me) {
for event in Social::new(
&self.show,
client,
&self.imgs,
&self.fonts,
&self.voxygen_i18n,
info.selected_entity,
&self.rot_imgs,
tooltip_manager,
)
.set(self.ids.social_window, ui_widgets)
{
match event {
social::Event::Close => {
self.show.social(false);
self.force_ungrab = true;
},
social::Event::ChangeSocialTab(social_tab) => {
self.show.open_social_tab(social_tab)
},
social::Event::Invite(uid) => events.push(Event::InviteMember(uid)),
}
}
}
}
// Group Window
for event in Group::new(
&mut self.show,
client,
&global_state.settings,
&self.imgs,
&self.fonts,
&self.voxygen_i18n,
self.pulse,
&global_state,
)
.set(self.ids.group_window, ui_widgets)
{
match event {
group::Event::Accept => events.push(Event::AcceptInvite),
group::Event::Decline => events.push(Event::DeclineInvite),
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
}
}
// Spellbook
if self.show.spell {

View File

@ -1,6 +1,6 @@
use super::{
img_ids::Imgs, FACTION_COLOR, GROUP_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, REGION_COLOR,
SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
img_ids::Imgs, DEFAULT_NPC, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR,
MANA_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
};
use crate::{
i18n::VoxygenLocalization,
@ -42,6 +42,7 @@ widget_ids! {
level_skull,
health_bar,
health_bar_bg,
health_txt,
mana_bar,
health_bar_fg,
}
@ -56,11 +57,13 @@ pub struct Overhead<'a> {
stats: &'a Stats,
energy: Option<&'a Energy>,
own_level: u32,
in_group: bool,
settings: &'a GameplaySettings,
pulse: f32,
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
#[conrod(common_builder)]
common: widget::CommonBuilder,
}
@ -73,6 +76,7 @@ impl<'a> Overhead<'a> {
stats: &'a Stats,
energy: Option<&'a Energy>,
own_level: u32,
in_group: bool,
settings: &'a GameplaySettings,
pulse: f32,
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
@ -85,6 +89,7 @@ impl<'a> Overhead<'a> {
stats,
energy,
own_level,
in_group,
settings,
pulse,
voxygen_i18n,
@ -104,13 +109,20 @@ impl<'a> Ingameable for Overhead<'a> {
// Number of conrod primitives contained in the overhead display. TODO maybe
// this could be done automatically?
// - 2 Text::new for name
// If HP Info is shown + 6
// - 1 for level: either Text or Image
// - 4 for HP + mana + fg + bg
// If there's a speech bubble
// - 2 Text::new for speech bubble
// - 1 for HP Text
// If there's a speech bubble + 13
// - 2 Text::new for speec8 bubble
// - 1 Image::new for icon
// - 10 Image::new for speech bubble (9-slice + tail)
7 + if self.bubble.is_some() { 13 } else { 0 }
2 + if self.bubble.is_some() { 13 } else { 0 }
+ if (self.stats.health.current() as f64 / self.stats.health.maximum() as f64) < 1.0 {
6
} else {
0
}
}
}
@ -134,19 +146,35 @@ impl<'a> Widget for Overhead<'a> {
const BARSIZE: f64 = 2.0;
const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
let hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
let name_y = if hp_percentage.abs() > 99.9 {
MANA_BAR_Y + 20.0
} else if level_comp > 9 {
MANA_BAR_Y + 38.0
} else {
MANA_BAR_Y + 32.0
};
let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
// Name
Text::new(&self.name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.font_size(font_size)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.x_y(-1.0, MANA_BAR_Y + 48.0)
.x_y(-1.0, name_y)
.parent(id)
.set(state.ids.name_bg, ui);
Text::new(&self.name)
.font_id(self.fonts.cyri.conrod_id)
.font_size(30)
.color(Color::Rgba(0.61, 0.61, 0.89, 1.0))
.x_y(0.0, MANA_BAR_Y + 50.0)
.font_size(font_size)
.color(if self.in_group {
GROUP_MEMBER
} else {
DEFAULT_NPC
})
.x_y(0.0, name_y + 1.0)
.parent(id)
.set(state.ids.name, ui);
// Speech bubble
@ -160,7 +188,7 @@ impl<'a> Widget for Overhead<'a> {
.color(text_color)
.font_id(self.fonts.cyri.conrod_id)
.font_size(18)
.up_from(state.ids.name, 20.0)
.up_from(state.ids.name, 26.0)
.x_align_to(state.ids.name, Align::Middle)
.parent(id);
@ -295,101 +323,121 @@ impl<'a> Widget for Overhead<'a> {
.set(state.ids.speech_bubble_icon, ui);
}
let hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
if hp_percentage < 100.0 {
// Show HP Bar
let hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
// Background
Image::new(self.imgs.enemy_health_bg)
// Background
Image::new(self.imgs.enemy_health_bg)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
.parent(id)
.set(state.ids.health_bar_bg, ui);
// % HP Filling
Image::new(self.imgs.enemy_bar)
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
.x_y(
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
MANA_BAR_Y + 7.5,
)
.color(Some(if hp_percentage <= 25.0 {
crit_hp_color
} else if hp_percentage <= 50.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.parent(id)
.set(state.ids.health_bar, ui);
// % HP Filling
Image::new(self.imgs.enemy_bar)
.w_h(73.0 * (hp_percentage / 100.0) * BARSIZE, 6.0 * BARSIZE)
.x_y(
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE,
MANA_BAR_Y + 7.5,
)
.color(Some(if hp_percentage <= 25.0 {
crit_hp_color
} else if hp_percentage <= 50.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.parent(id)
.set(state.ids.health_bar, ui);
// TODO Only show health values for entities below 100% health
let mut txt = format!(
"{}/{}",
self.stats.health.current().max(1) / 10 as u32, /* Don't show 0 health for
* living entities */
self.stats.health.maximum() / 10 as u32,
);
if self.stats.is_dead {
txt = self.voxygen_i18n.get("hud.group.dead").to_string()
};
Text::new(&txt)
.mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
.font_size(10)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.parent(id)
.set(state.ids.health_txt, ui);
// % Mana Filling
if let Some(energy) = self.energy {
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
// % Mana Filling
if let Some(energy) = self.energy {
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
Rectangle::fill_with(
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
MANA_COLOR,
)
.x_y(
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,
MANA_BAR_Y, //-32.0,
)
.parent(id)
.set(state.ids.mana_bar, ui);
}
Rectangle::fill_with(
[72.0 * energy_factor * BARSIZE, MANA_BAR_HEIGHT],
MANA_COLOR,
)
.x_y(
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE,
MANA_BAR_Y, //-32.0,
)
.parent(id)
.set(state.ids.mana_bar, ui);
}
// Foreground
Image::new(self.imgs.enemy_health)
// Foreground
Image::new(self.imgs.enemy_health)
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
.parent(id)
.set(state.ids.health_bar_fg, ui);
// Level
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Change visuals of the level display depending on the player level/opponent
// level
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
// + 10 level above player -> skull
// + 5-10 levels above player -> high
// -5 - +5 levels around player level -> equal
// - 5 levels below player -> low
if level_comp > 9 {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level_skull, ui);
} else {
Text::new(&format!("{}", self.stats.level.level()))
.font_id(self.fonts.cyri.conrod_id)
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
14
// Level
const LOW: Color = Color::Rgba(0.54, 0.81, 0.94, 0.4);
const HIGH: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
const EQUAL: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Change visuals of the level display depending on the player level/opponent
// level
let level_comp = self.stats.level.level() as i64 - self.own_level as i64;
// + 10 level above player -> skull
// + 5-10 levels above player -> high
// -5 - +5 levels around player level -> equal
// - 5 levels below player -> low
if level_comp > 9 {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
15
self.imgs.skull
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level, ui);
.set(state.ids.level_skull, ui);
} else {
Text::new(&format!("{}", self.stats.level.level()))
.font_id(self.fonts.cyri.conrod_id)
.font_size(if self.stats.level.level() > 9 && level_comp < 10 {
14
} else {
15
})
.color(if level_comp > 4 {
HIGH
} else if level_comp < -5 {
LOW
} else {
EQUAL
})
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 9.0)
.parent(id)
.set(state.ids.level, ui);
}
}
}
}

View File

@ -1190,6 +1190,10 @@ impl<'a> Widget for SettingsWindow<'a> {
.color(TEXT_COLOR)
.set(state.ids.chat_char_name_text, ui);
// TODO Show account name in chat
// TODO Show account names in social window
// Language select drop down
Text::new(&self.localized_strings.get("common.languages"))
.down_from(state.ids.chat_char_name_button, 20.0)

View File

@ -219,10 +219,14 @@ impl<'a> Widget for Skillbar<'a> {
let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64);
let hp_percentage =
let mut hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
let mut energy_percentage =
self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
if self.stats.is_dead {
hp_percentage = 0.0;
energy_percentage = 0.0;
};
let scale = 2.0;
let bar_values = self.global_state.settings.gameplay.bar_numbers;
@ -1153,15 +1157,14 @@ impl<'a> Widget for Skillbar<'a> {
.w_h(100.0 * scale, 20.0 * scale)
.top_left_with_margins_on(state.ids.m1_slot, 0.0, -100.0 * scale)
.set(state.ids.healthbar_bg, ui);
let health_col = match hp_percentage as u8 {
0..=20 => crit_hp_color,
21..=40 => LOW_HP_COLOR,
_ => HP_COLOR,
};
Image::new(self.imgs.bar_content)
.w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale)
.color(Some(if hp_percentage <= 20.0 {
crit_hp_color
} else if hp_percentage <= 40.0 {
LOW_HP_COLOR
} else {
HP_COLOR
}))
.color(Some(health_col))
.top_right_with_margins_on(state.ids.healthbar_bg, 2.0 * scale, 1.0 * scale)
.set(state.ids.healthbar_filling, ui);
// Energybar
@ -1181,11 +1184,22 @@ impl<'a> Widget for Skillbar<'a> {
// Bar Text
// Values
if let BarNumbers::Values = bar_values {
let hp_text = format!(
let mut hp_text = format!(
"{}/{}",
(self.stats.health.current() / 10) as u32,
(self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for
* living players */
(self.stats.health.maximum() / 10) as u32
);
let mut energy_text = format!(
"{}/{}",
self.energy.current() as u32 / 10, /* TODO Fix regeneration with smaller energy
* numbers instead of dividing by 10 here */
self.energy.maximum() as u32 / 10
);
if self.stats.is_dead {
hp_text = self.localized_strings.get("hud.group.dead").to_string();
energy_text = self.localized_strings.get("hud.group.dead").to_string();
};
Text::new(&hp_text)
.mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale)
.font_size(self.fonts.cyri.scale(14))
@ -1198,12 +1212,6 @@ impl<'a> Widget for Skillbar<'a> {
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.health_text, ui);
let energy_text = format!(
"{}/{}",
self.energy.current() as u32 / 10, /* TODO Fix regeneration with smaller energy
* numbers instead of dividing by 10 here */
self.energy.maximum() as u32 / 10
);
Text::new(&energy_text)
.mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale)
.font_size(self.fonts.cyri.scale(14))

View File

@ -1,33 +1,60 @@
use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN};
use super::{
img_ids::{Imgs, ImgsRot},
Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts};
use crate::{
i18n::VoxygenLocalization,
ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable},
};
use client::{self, Client};
use common::{comp::group, sync::Uid};
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
};
use std::time::Instant;
widget_ids! {
pub struct Ids {
social_frame,
social_close,
social_title,
frame,
align,
content_align,
online_tab,
friends_tab,
faction_tab,
online_title,
online_no,
close,
title_align,
title,
bg,
icon,
scrollbar,
online_align,
online_tab,
names_align,
name_txt,
player_levels[],
player_names[],
player_zones[],
online_txt,
online_no,
levels_align,
level_txt,
zones_align,
zone_txt,
friends_tab,
//friends_tab_icon,
faction_tab,
//faction_tab_icon,
friends_test,
faction_test,
player_names[],
invite_button,
}
}
pub struct State {
ids: Ids,
// Holds the time when selection is made since this selection can be overriden
// by selecting an entity in-game
selected_uid: Option<(Uid, Instant)>,
}
pub enum SocialTab {
Online,
Friends,
@ -41,25 +68,35 @@ pub struct Social<'a> {
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
selected_entity: Option<(specs::Entity, Instant)>,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
#[conrod(common_builder)]
common: widget::CommonBuilder,
}
impl<'a> Social<'a> {
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
pub fn new(
show: &'a Show,
client: &'a Client,
imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts,
localized_strings: &'a std::sync::Arc<VoxygenLocalization>,
selected_entity: Option<(specs::Entity, Instant)>,
rot_imgs: &'a ImgsRot,
tooltip_manager: &'a mut TooltipManager,
) -> Self {
Self {
show,
client,
imgs,
rot_imgs,
fonts,
localized_strings,
tooltip_manager,
selected_entity,
common: widget::CommonBuilder::default(),
}
}
@ -67,223 +104,467 @@ impl<'a> Social<'a> {
pub enum Event {
Close,
Invite(Uid),
ChangeSocialTab(SocialTab),
}
impl<'a> Widget for Social<'a> {
type Event = Vec<Event>;
type State = Ids;
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Ids::new(id_gen) }
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
Self::State {
ids: Ids::new(id_gen),
selected_uid: None,
}
}
#[allow(clippy::unused_unit)] // TODO: Pending review in #587
fn style(&self) -> Self::Style { () }
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs {
/* id, */ state: ids,
ui,
..
} = args;
let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new();
let button_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
let edge = &self.rot_imgs.tt_side;
let corner = &self.rot_imgs.tt_corner;
ImageFrame::new(
[edge.cw180, edge.none, edge.cw270, edge.cw90],
[corner.none, corner.cw270, corner.cw90, corner.cw180],
Color::Rgba(0.08, 0.07, 0.04, 1.0),
5.0,
)
})
.title_font_size(self.fonts.cyri.scale(15))
.parent(ui.window)
.desc_font_size(self.fonts.cyri.scale(12))
.title_text_color(TEXT_COLOR)
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
Image::new(self.imgs.window_3)
.top_left_with_margins_on(ui.window, 200.0, 25.0)
// Window frame and BG
let pos = if self.show.group || self.show.group_menu {
200.0
} else {
25.0
};
// TODO: Different window visuals depending on the selected tab
let window_bg = match &self.show.social_tab {
SocialTab::Online => self.imgs.social_bg_on,
SocialTab::Friends => self.imgs.social_bg_friends,
SocialTab::Faction => self.imgs.social_bg_fact,
};
let window_frame = match &self.show.social_tab {
SocialTab::Online => self.imgs.social_frame_on,
SocialTab::Friends => self.imgs.social_frame_friends,
SocialTab::Faction => self.imgs.social_frame_fact,
};
Image::new(window_bg)
.bottom_left_with_margins_on(ui.window, 308.0, pos)
.color(Some(UI_MAIN))
.w_h(103.0 * 4.0, 122.0 * 4.0)
.set(ids.social_frame, ui);
.w_h(280.0, 460.0)
.set(state.ids.bg, ui);
Image::new(window_frame)
.middle_of(state.ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.w_h(280.0, 460.0)
.set(state.ids.frame, ui);
// Icon
Image::new(self.imgs.social)
.w_h(30.0, 30.0)
.top_left_with_margins_on(state.ids.frame, 6.0, 6.0)
.set(state.ids.icon, ui);
// X-Button
if Button::image(self.imgs.close_button)
.w_h(28.0, 28.0)
.w_h(24.0, 25.0)
.hover_image(self.imgs.close_button_hover)
.press_image(self.imgs.close_button_press)
.top_right_with_margins_on(ids.social_frame, 0.0, 0.0)
.set(ids.social_close, ui)
.top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
.set(state.ids.close, ui)
.was_clicked()
{
events.push(Event::Close);
}
// Title
Rectangle::fill_with([212.0, 42.0], color::TRANSPARENT)
.top_left_with_margins_on(state.ids.frame, 2.0, 44.0)
.set(state.ids.title_align, ui);
Text::new(&self.localized_strings.get("hud.social"))
.mid_top_with_margin_on(ids.social_frame, 6.0)
.middle_of(state.ids.title_align)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(ids.social_title, ui);
.set(state.ids.title, ui);
// Alignment
Rectangle::fill_with([99.0 * 4.0, 112.0 * 4.0], color::TRANSPARENT)
.mid_top_with_margin_on(ids.social_frame, 8.0 * 4.0)
.set(ids.align, ui);
// Content Alignment
Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT)
.middle_of(ids.frame)
.scroll_kids()
.scroll_kids_vertically()
.set(ids.content_align, ui);
Scrollbar::y_axis(ids.content_align)
.thickness(5.0)
.rgba(0.33, 0.33, 0.33, 1.0)
.set(ids.scrollbar, ui);
// Frame
Image::new(self.imgs.social_frame)
.w_h(99.0 * 4.0, 100.0 * 4.0)
.mid_bottom_of(ids.align)
.color(Some(UI_MAIN))
.set(ids.frame, ui);
// Online Tab
if Button::image(if let SocialTab::Online = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button
// Tabs Buttons
// Online Tab Button
if Button::image(match &self.show.social_tab {
SocialTab::Online => self.imgs.social_tab_online,
_ => self.imgs.social_tab_inact,
})
.w_h(30.0 * 4.0, 12.0 * 4.0)
.hover_image(if let SocialTab::Online = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button_hover
.w_h(30.0, 44.0)
.image_color(match &self.show.social_tab {
SocialTab::Online => UI_MAIN,
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
})
.press_image(if let SocialTab::Online = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button_press
})
.top_left_with_margins_on(ids.align, 4.0, 0.0)
.label(&self.localized_strings.get("hud.social.online"))
.label_font_size(self.fonts.cyri.scale(14))
.label_font_id(self.fonts.cyri.conrod_id)
.parent(ids.frame)
.color(UI_MAIN)
.label_color(TEXT_COLOR)
.set(ids.online_tab, ui)
.top_right_with_margins_on(state.ids.frame, 50.0, -27.0)
.set(state.ids.online_tab, ui)
.was_clicked()
{
events.push(Event::ChangeSocialTab(SocialTab::Online));
}
// Contents
if let SocialTab::Online = self.show.social_tab {
// TODO Needs to be a string sent from the server
// Players list
// TODO: this list changes infrequently enough that it should not have to be
// recreated every frame
let players = self.client.player_list.values().filter(|p| p.is_online);
let count = players.clone().count();
if ids.player_names.len() < count {
ids.update(|ids| {
ids.player_names
.resize(count, &mut ui.widget_id_generator())
})
}
Text::new(
&self
.localized_strings
.get("hud.social.play_online_fmt")
.replace("{nb_player}", &format!("{:?}", count)),
)
.top_left_with_margins_on(ids.content_align, -2.0, 7.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(ids.online_title, ui);
for (i, player_info) in players.enumerate() {
Text::new(&format!(
"[{}] {}",
player_info.player_alias,
match &player_info.character {
Some(character) => format!("{} Lvl {}", &character.name, &character.level),
None => "<None>".to_string(), // character select or spectating
}
))
.down(3.0)
.font_size(self.fonts.cyri.scale(15))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(ids.player_names[i], ui);
}
}
// Friends Tab
if Button::image(if let SocialTab::Friends = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button
// Friends Tab Button
if Button::image(match &self.show.social_tab {
SocialTab::Friends => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact,
})
.w_h(30.0 * 4.0, 12.0 * 4.0)
.hover_image(if let SocialTab::Friends = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button
.w_h(30.0, 44.0)
.hover_image(match &self.show.social_tab {
SocialTab::Friends => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact_hover,
})
.press_image(if let SocialTab::Friends = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button
.press_image(match &self.show.social_tab {
SocialTab::Friends => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact_press,
})
.right_from(ids.online_tab, 0.0)
.label(&self.localized_strings.get("hud.social.friends"))
.label_font_size(self.fonts.cyri.scale(14))
.label_font_id(self.fonts.cyri.conrod_id)
.parent(ids.frame)
.color(UI_MAIN)
.label_color(TEXT_COLOR_3)
.set(ids.friends_tab, ui)
.down_from(state.ids.online_tab, 0.0)
.image_color(match &self.show.social_tab {
SocialTab::Friends => UI_MAIN,
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
})
.set(state.ids.friends_tab, ui)
.was_clicked()
{
events.push(Event::ChangeSocialTab(SocialTab::Friends));
}
// Contents
if let SocialTab::Friends = self.show.social_tab {
Text::new(&self.localized_strings.get("hud.social.not_yet_available"))
.middle_of(ids.content_align)
.font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR_3)
.set(ids.friends_test, ui);
}
// Faction Tab
let button_img = if let SocialTab::Faction = self.show.social_tab {
self.imgs.social_button_pressed
} else {
self.imgs.social_button
};
if Button::image(button_img)
.w_h(30.0 * 4.0, 12.0 * 4.0)
.right_from(ids.friends_tab, 0.0)
.label(&self.localized_strings.get("hud.social.faction"))
.parent(ids.frame)
.label_font_size(self.fonts.cyri.scale(14))
.label_font_id(self.fonts.cyri.conrod_id)
.color(UI_MAIN)
.label_color(TEXT_COLOR_3)
.set(ids.faction_tab, ui)
.was_clicked()
// Faction Tab Button
if Button::image(match &self.show.social_tab {
SocialTab::Friends => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact,
})
.w_h(30.0, 44.0)
.hover_image(match &self.show.social_tab {
SocialTab::Faction => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact_hover,
})
.press_image(match &self.show.social_tab {
SocialTab::Faction => self.imgs.social_tab_act,
_ => self.imgs.social_tab_inact_press,
})
.down_from(state.ids.friends_tab, 0.0)
.image_color(match &self.show.social_tab {
SocialTab::Faction => UI_MAIN,
_ => Color::Rgba(1.0, 1.0, 1.0, 0.6),
})
.set(state.ids.faction_tab, ui)
.was_clicked()
{
events.push(Event::ChangeSocialTab(SocialTab::Faction));
}
// Contents
if let SocialTab::Faction = self.show.social_tab {
Text::new(&self.localized_strings.get("hud.social.not_yet_available"))
.middle_of(ids.content_align)
.font_size(self.fonts.cyri.scale(18))
// Online Tab
if let SocialTab::Online = self.show.social_tab {
// Content Alignments
Rectangle::fill_with([270.0, 346.0], color::TRANSPARENT)
.mid_top_with_margin_on(state.ids.frame, 74.0)
.scroll_kids_vertically()
.set(state.ids.online_align, ui);
Rectangle::fill_with([133.0, 346.0], color::TRANSPARENT)
.top_left_with_margins_on(state.ids.online_align, 0.0, 0.0)
.crop_kids()
.set(state.ids.names_align, ui);
Rectangle::fill_with([39.0, 346.0], color::TRANSPARENT)
.right_from(state.ids.names_align, 2.0)
.crop_kids()
.set(state.ids.levels_align, ui);
Rectangle::fill_with([94.0, 346.0], color::TRANSPARENT)
.right_from(state.ids.levels_align, 2.0)
.crop_kids()
.set(state.ids.zones_align, ui);
Scrollbar::y_axis(state.ids.online_align)
.thickness(4.0)
.color(Color::Rgba(0.79, 1.09, 1.09, 0.0))
.set(state.ids.scrollbar, ui);
//
// Headlines
//
if Button::image(self.imgs.nothing)
.w_h(133.0, 18.0)
.top_left_with_margins_on(state.ids.frame, 52.0, 7.0)
.label(&self.localized_strings.get("hud.social.name"))
.label_font_size(self.fonts.cyri.scale(14))
.label_y(conrod_core::position::Relative::Scalar(0.0))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.set(state.ids.name_txt, ui)
.was_clicked()
{
// Sort widgets by name alphabetically
}
if Button::image(self.imgs.nothing)
.w_h(39.0, 18.0)
.right_from(state.ids.name_txt, 2.0)
.label(&self.localized_strings.get("hud.social.level"))
.label_font_size(self.fonts.cyri.scale(14))
.label_y(conrod_core::position::Relative::Scalar(0.0))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.set(state.ids.level_txt, ui)
.was_clicked()
{
// Sort widgets by level (increasing)
}
if Button::image(self.imgs.nothing)
.w_h(93.0, 18.0)
.right_from(state.ids.level_txt, 2.0)
.label(&self.localized_strings.get("hud.social.zone"))
.label_font_size(self.fonts.cyri.scale(14))
.label_y(conrod_core::position::Relative::Scalar(0.0))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.set(state.ids.zone_txt, ui)
.was_clicked()
{
// Sort widgets by zone alphabetically
}
// Online Text
let players = self.client.player_list.iter().filter(|(_, p)| p.is_online);
let count = players.clone().count();
Text::new(&self.localized_strings.get("hud.social.online"))
.bottom_left_with_margins_on(state.ids.frame, 18.0, 10.0)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR_3)
.set(ids.faction_test, ui);
}
.font_size(self.fonts.cyri.scale(14))
.color(TEXT_COLOR)
.set(state.ids.online_txt, ui);
Text::new(&(count - 1).to_string())
.right_from(state.ids.online_txt, 5.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.color(TEXT_COLOR)
.set(state.ids.online_no, ui);
// Adjust widget_id struct vec length to player count
if state.ids.player_levels.len() < count {
state.update(|s| {
s.ids
.player_levels
.resize(count, &mut ui.widget_id_generator())
})
};
if state.ids.player_names.len() < count {
state.update(|s| {
s.ids
.player_names
.resize(count, &mut ui.widget_id_generator())
})
};
if state.ids.player_zones.len() < count {
state.update(|s| {
s.ids
.player_zones
.resize(count, &mut ui.widget_id_generator())
})
};
// Create a name, level and zone row for every player in the list
// Filter out yourself from the online list
let my_uid = self.client.uid();
for (i, (&uid, player_info)) in
players.filter(|(uid, _)| Some(**uid) != my_uid).enumerate()
{
let hide_username = true;
let zone = "Wilderness"; // TODO Add real zone
let selected = state.selected_uid.map_or(false, |u| u.0 == uid);
let alias = &player_info.player_alias;
let name_text = match &player_info.character {
Some(character) => {
if Some(uid) == my_uid {
format!(
"{} ({})",
&self.localized_strings.get("hud.common.you"),
&character.name
)
} else if hide_username {
character.name.clone()
} else {
format!("[{}] {}", alias, &character.name)
}
},
None => alias.clone(), // character select or spectating
};
let level = match &player_info.character {
Some(character) => format!("{} ", &character.level),
None => "".to_string(), // character select or spectating
};
let zone_name = match &player_info.character {
None => self.localized_strings.get("hud.group.in_menu").to_string(), /* character select or spectating */
_ => format!("{} ", &zone),
};
// Player name widgets
let button = Button::image(if !selected {
self.imgs.nothing
} else {
self.imgs.selection
});
let button = if i == 0 {
button.mid_top_with_margin_on(state.ids.names_align, 1.0)
} else {
button.down_from(state.ids.player_names[i - 1], 1.0)
};
button
.w_h(133.0, 20.0)
.hover_image(if selected {
self.imgs.selection
} else {
self.imgs.selection_hover
})
.press_image(if selected {
self.imgs.selection
} else {
self.imgs.selection_press
})
.label(&name_text)
.label_font_size(self.fonts.cyri.scale(14))
.label_y(conrod_core::position::Relative::Scalar(1.0))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.set(state.ids.player_names[i], ui);
// Player Levels
Button::image(if !selected {
self.imgs.nothing
} else {
self.imgs.selection
})
.w_h(39.0, 20.0)
.right_from(state.ids.player_names[i], 2.0)
.label(&level)
.label_font_size(self.fonts.cyri.scale(14))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.label_y(conrod_core::position::Relative::Scalar(1.0))
.parent(state.ids.levels_align)
.set(state.ids.player_levels[i], ui);
// Player Zones
Button::image(if !selected {
self.imgs.nothing
} else {
self.imgs.selection
})
.w_h(94.0, 20.0)
.right_from(state.ids.player_levels[i], 2.0)
.label(&zone_name)
.label_font_size(self.fonts.cyri.scale(14))
.label_font_id(self.fonts.cyri.conrod_id)
.label_color(TEXT_COLOR)
.label_y(conrod_core::position::Relative::Scalar(1.0))
.parent(state.ids.zones_align)
.set(state.ids.player_zones[i], ui);
// Check for click
if ui
.widget_input(state.ids.player_names[i])
.clicks()
.left()
.next()
.is_some()
{
state.update(|s| s.selected_uid = Some((uid, Instant::now())));
}
}
// Invite Button
let is_leader_or_not_in_group = self
.client
.group_info()
.map_or(true, |(_, l_uid)| self.client.uid() == Some(l_uid));
let current_members = self
.client
.group_members()
.iter()
.filter(|(_, role)| matches!(role, group::Role::Member))
.count()
+ 1;
let current_invites = self.client.pending_invites().len();
let max_members = self.client.max_group_size() as usize;
let group_not_full = current_members + current_invites < max_members;
let selected_to_invite = (is_leader_or_not_in_group && group_not_full)
.then(|| {
state
.selected_uid
.as_ref()
.map(|(s, _)| *s)
.filter(|selected| {
self.client
.player_list
.get(selected)
.map_or(false, |selected_player| {
selected_player.is_online && selected_player.character.is_some()
})
})
.or_else(|| {
self.selected_entity
.and_then(|s| self.client.state().read_component_copied(s.0))
})
.filter(|selected| {
// Prevent inviting entities already in the same group
!self.client.group_members().contains_key(selected)
})
})
.flatten();
let invite_button = Button::image(self.imgs.button)
.w_h(106.0, 26.0)
.bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0)
.hover_image(if selected_to_invite.is_some() {
self.imgs.button_hover
} else {
self.imgs.button
})
.press_image(if selected_to_invite.is_some() {
self.imgs.button_press
} else {
self.imgs.button
})
.label(self.localized_strings.get("hud.group.invite"))
.label_y(conrod_core::position::Relative::Scalar(3.0))
.label_color(if selected_to_invite.is_some() {
TEXT_COLOR
} else {
TEXT_COLOR_3
})
.image_color(if selected_to_invite.is_some() {
TEXT_COLOR
} else {
TEXT_COLOR_3
})
.label_font_size(self.fonts.cyri.scale(15))
.label_font_id(self.fonts.cyri.conrod_id);
if if self.client.group_info().is_some() {
let tooltip_txt = format!(
"{}/{} {}",
current_members + current_invites,
max_members,
&self.localized_strings.get("hud.group.members")
);
invite_button
.with_tooltip(self.tooltip_manager, &tooltip_txt, "", &button_tooltip)
.set(state.ids.invite_button, ui)
} else {
invite_button.set(state.ids.invite_button, ui)
}
.was_clicked()
{
if let Some(uid) = selected_to_invite {
events.push(Event::Invite(uid));
state.update(|s| {
s.selected_uid = None;
});
}
}
} // End of Online Tab
events
}

View File

@ -22,8 +22,8 @@ use anim::{
};
use common::{
comp::{
item::ItemKind, Body, CharacterState, Last, LightAnimation, LightEmitter, Loadout, Ori,
PhysicsState, Pos, Scale, Stats, Vel,
item::ItemKind, Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout,
Ori, PhysicsState, Pos, Scale, Stats, Vel,
},
state::{DeltaTime, State},
states::triple_strike,
@ -192,7 +192,6 @@ impl FigureMgr {
.read_storage::<Pos>()
.get(scene_data.player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
for (
i,
(
@ -207,6 +206,7 @@ impl FigureMgr {
physics,
stats,
loadout,
item,
),
) in (
&ecs.entities(),
@ -220,6 +220,7 @@ impl FigureMgr {
&ecs.read_storage::<PhysicsState>(),
ecs.read_storage::<Stats>().maybe(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Item>().maybe(),
)
.join()
.enumerate()
@ -519,7 +520,13 @@ impl FigureMgr {
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32
})
})
.unwrap_or(Rgba::broadcast(1.0));
.unwrap_or(Rgba::broadcast(1.0))
// Highlight targeted collectible entities
* if item.is_some() && scene_data.target_entity.map_or(false, |e| e == entity) {
Rgba::new(2.0, 2.0, 2.0, 1.0)
} else {
Rgba::one()
};
let scale = scale.map(|s| s.0).unwrap_or(1.0);

View File

@ -70,6 +70,7 @@ pub struct Scene {
pub struct SceneData<'a> {
pub state: &'a State,
pub player_entity: specs::Entity,
pub target_entity: Option<specs::Entity>,
pub loaded_distance: f32,
pub view_distance: u32,
pub tick: u64,

View File

@ -52,6 +52,8 @@ pub struct SessionState {
free_look: bool,
auto_walk: 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).
@ -86,6 +88,8 @@ impl SessionState {
free_look: false,
auto_walk: false,
is_aiming: false,
target_entity: None,
selected_entity: None,
}
}
@ -208,18 +212,6 @@ impl PlayState for SessionState {
view_mat, cam_pos, ..
} = self.scene.camera().dependents();
// Choose a spot above the player's head for item distance checks
let player_pos = match self
.client
.borrow()
.state()
.read_storage::<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 client = self.client.borrow();
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());
// Check to see whether we're aiming at anything
let (build_pos, select_pos) = {
let client = self.client.borrow();
let terrain = client.state().terrain();
let cam_ray = terrain
.ray(cam_pos, cam_pos + cam_dir * 100.0)
.until(|block| block.is_tangible())
.cast();
let cam_dist = cam_ray.0;
match cam_ray.1 {
Ok(Some(_))
if player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
<= MAX_PICKUP_RANGE_SQR =>
{
(
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)),
)
},
_ => (None, None),
}
};
let (build_pos, select_pos, target_entity) =
under_cursor(&self.client.borrow(), cam_pos, cam_dir);
// Throw out distance info, it will be useful in the future
self.target_entity = target_entity.map(|x| x.0);
let can_build = self
.client
@ -559,6 +531,24 @@ impl PlayState for SessionState {
let camera = self.scene.camera_mut();
camera.next_mode(self.client.borrow().is_admin());
},
Event::InputUpdate(GameInput::Select, state) => {
if !state {
self.selected_entity =
self.target_entity.map(|e| (e, std::time::Instant::now()));
}
},
Event::InputUpdate(GameInput::AcceptGroupInvite, true) => {
let mut client = self.client.borrow_mut();
if client.group_invite().is_some() {
client.accept_group_invite();
}
},
Event::InputUpdate(GameInput::DeclineGroupInvite, true) => {
let mut client = self.client.borrow_mut();
if client.group_invite().is_some() {
client.decline_group_invite();
}
},
Event::AnalogGameInput(input) => match input {
AnalogGameInput::MovementX(v) => {
self.key_state.analog_matrix.x = v;
@ -708,6 +698,8 @@ impl PlayState for SessionState {
self.scene.camera().get_mode(),
camera::CameraMode::FirstPerson
),
target_entity: self.target_entity,
selected_entity: self.selected_entity,
},
);
@ -971,6 +963,24 @@ impl PlayState for SessionState {
HudEvent::CraftRecipe(r) => {
self.client.borrow_mut().craft_recipe(&r);
},
HudEvent::InviteMember(uid) => {
self.client.borrow_mut().send_group_invite(uid);
},
HudEvent::AcceptInvite => {
self.client.borrow_mut().accept_group_invite();
},
HudEvent::DeclineInvite => {
self.client.borrow_mut().decline_group_invite();
},
HudEvent::KickMember(uid) => {
self.client.borrow_mut().kick_from_group(uid);
},
HudEvent::LeaveGroup => {
self.client.borrow_mut().leave_group();
},
HudEvent::AssignLeader(uid) => {
self.client.borrow_mut().assign_group_leader(uid);
},
}
}
@ -979,6 +989,7 @@ impl PlayState for SessionState {
let scene_data = SceneData {
state: client.state(),
player_entity: client.entity(),
target_entity: self.target_entity,
loaded_distance: client.loaded_distance(),
view_distance: client.view_distance().unwrap_or(1),
tick: client.get_tick(),
@ -1033,6 +1044,7 @@ impl PlayState for SessionState {
let scene_data = SceneData {
state: client.state(),
player_entity: client.entity(),
target_entity: self.target_entity,
loaded_distance: client.loaded_distance(),
view_distance: client.view_distance().unwrap_or(1),
tick: client.get_tick(),
@ -1055,3 +1067,108 @@ impl PlayState for SessionState {
self.hud.render(renderer, self.scene.globals());
}
}
/// Max distance an entity can be "targeted"
const MAX_TARGET_RANGE: f32 = 30.0;
/// Calculate what the cursor is pointing at within the 3d scene
#[allow(clippy::type_complexity)]
fn under_cursor(
client: &Client,
cam_pos: Vec3<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)
}

View File

@ -169,6 +169,9 @@ impl ControlSettings {
GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9),
GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q),
GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt),
GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y),
GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U),
GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I),
}
}
}
@ -234,6 +237,9 @@ impl Default for ControlSettings {
GameInput::Slot9,
GameInput::Slot10,
GameInput::SwapLoadout,
GameInput::Select,
GameInput::AcceptGroupInvite,
GameInput::DeclineGroupInvite,
];
for game_input in game_inputs {
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));

View File

@ -67,6 +67,9 @@ pub enum GameInput {
FreeLook,
AutoWalk,
CycleCamera,
Select,
AcceptGroupInvite,
DeclineGroupInvite,
}
impl GameInput {
@ -123,6 +126,9 @@ impl GameInput {
GameInput::Slot9 => "gameinput.slot9",
GameInput::Slot10 => "gameinput.slot10",
GameInput::SwapLoadout => "gameinput.swaploadout",
GameInput::Select => "gameinput.select",
GameInput::AcceptGroupInvite => "gameinput.acceptgroupinvite",
GameInput::DeclineGroupInvite => "gameinput.declinegroupinvite",
}
}

View File

@ -522,7 +522,7 @@ impl Floor {
"common.items.armor.shoulder.cultist_shoulder_purple",
),
8 => comp::Item::expect_from_asset(
"common.items.weapons.sword.greatsword_2h_fine-0",
"common.items.weapons.staff.cultist_staff",
),
9 => comp::Item::expect_from_asset(
"common.items.weapons.sword.greatsword_2h_fine-1",