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

1
Cargo.lock generated
View File

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

View File

@ -5,7 +5,7 @@ Item(
( (
kind: Back("Short0"), kind: Back("Short0"),
stats: ( 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( Item(
name: "Uneven Bow", name: "Uneven Bow",
description: "Someone carved his initials into it.", description: "Someone carved their initials into it.",
kind: Tool( kind: Tool(
( (
kind: Bow("ShortBow0"), kind: Bow("ShortBow0"),

View File

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

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

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

View File

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

View File

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

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

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -17,13 +17,13 @@ use byteorder::{ByteOrder, LittleEndian};
use common::{ use common::{
character::CharacterItem, character::CharacterItem,
comp::{ comp::{
self, ControlAction, ControlEvent, Controller, ControllerInputs, InventoryManip, self, group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
InventoryUpdateEvent, InventoryManip, InventoryUpdateEvent,
}, },
msg::{ msg::{
validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, InviteAnswer,
PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo,
MAX_BYTES_CHAT_MSG, ServerMsg, MAX_BYTES_CHAT_MSG,
}, },
recipe::RecipeBook, recipe::RecipeBook,
state::State, state::State,
@ -79,6 +79,15 @@ pub struct Client {
recipe_book: RecipeBook, recipe_book: RecipeBook,
available_recipes: HashSet<String>, available_recipes: HashSet<String>,
max_group_size: u32,
// Client has received an invite (inviter uid, time out instant)
group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>,
group_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
// Pending invites that this client has sent out
pending_invites: HashSet<Uid>,
_network: Network, _network: Network,
participant: Option<Participant>, participant: Option<Participant>,
singleton_stream: Stream, singleton_stream: Stream,
@ -126,47 +135,49 @@ impl Client {
let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?; let mut stream = block_on(participant.open(10, PROMISES_ORDERED | PROMISES_CONSISTENCY))?;
// Wait for initial sync // Wait for initial sync
let (state, entity, server_info, world_map, recipe_book) = block_on(async { let (state, entity, server_info, world_map, recipe_book, max_group_size) = block_on(
loop { async {
match stream.recv().await? { loop {
ServerMsg::InitialSync { match stream.recv().await? {
entity_package, ServerMsg::InitialSync {
server_info, entity_package,
time_of_day, server_info,
world_map: (map_size, world_map), time_of_day,
recipe_book, max_group_size,
} => { world_map: (map_size, world_map),
// TODO: Display that versions don't match in Voxygen recipe_book,
if &server_info.git_hash != *common::util::GIT_HASH { } => {
warn!( // TODO: Display that versions don't match in Voxygen
"Server is running {}[{}], you are running {}[{}], versions might \ if &server_info.git_hash != *common::util::GIT_HASH {
be incompatible!", warn!(
server_info.git_hash, "Server is running {}[{}], you are running {}[{}], versions \
server_info.git_date, might be incompatible!",
common::util::GIT_HASH.to_string(), server_info.git_hash,
common::util::GIT_DATE.to_string(), server_info.git_date,
); common::util::GIT_HASH.to_string(),
} common::util::GIT_DATE.to_string(),
);
}
debug!("Auth Server: {:?}", server_info.auth_provider); debug!("Auth Server: {:?}", server_info.auth_provider);
// Initialize `State` // Initialize `State`
let mut state = State::default(); let mut state = State::default();
// Client-only components // Client-only components
state state
.ecs_mut() .ecs_mut()
.register::<comp::Last<comp::CharacterState>>(); .register::<comp::Last<comp::CharacterState>>();
let entity = state.ecs_mut().apply_entity_package(entity_package); let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day; *state.ecs_mut().write_resource() = time_of_day;
assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize); assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize);
let mut world_map_raw = let mut world_map_raw =
vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/]; vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/];
LittleEndian::write_u32_into(&world_map, &mut world_map_raw); LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
debug!("Preparing image..."); debug!("Preparing image...");
let world_map = Arc::new( let world_map = Arc::new(
image::DynamicImage::ImageRgba8({ image::DynamicImage::ImageRgba8({
// Should not fail if the dimensions are correct. // Should not fail if the dimensions are correct.
let world_map = let world_map =
image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw); image::ImageBuffer::from_raw(map_size.x, map_size.y, world_map_raw);
@ -175,24 +186,26 @@ impl Client {
// Flip the image, since Voxygen uses an orientation where rotation from // Flip the image, since Voxygen uses an orientation where rotation from
// positive x axis to positive y axis is counterclockwise around the z axis. // positive x axis to positive y axis is counterclockwise around the z axis.
.flipv(), .flipv(),
); );
debug!("Done preparing image..."); debug!("Done preparing image...");
break Ok(( break Ok((
state, state,
entity, entity,
server_info, server_info,
(world_map, map_size), (world_map, map_size),
recipe_book, recipe_book,
)); max_group_size,
}, ));
ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers), },
err => { ServerMsg::TooManyPlayers => break Err(Error::TooManyPlayers),
warn!("whoops, server mad {:?}, ignoring", err); err => {
}, warn!("whoops, server mad {:?}, ignoring", err);
},
}
} }
} },
})?; )?;
stream.send(ClientMsg::Ping)?; stream.send(ClientMsg::Ping)?;
@ -213,6 +226,12 @@ impl Client {
recipe_book, recipe_book,
available_recipes: HashSet::default(), available_recipes: HashSet::default(),
max_group_size,
group_invite: None,
group_leader: None,
group_members: HashMap::new(),
pending_invites: HashSet::new(),
_network: network, _network: network,
participant: Some(participant), participant: Some(participant),
singleton_stream: stream, singleton_stream: stream,
@ -375,7 +394,7 @@ impl Client {
} }
pub fn pick_up(&mut self, entity: EcsEntity) { pub fn pick_up(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() { if let Some(uid) = self.state.read_component_copied(entity) {
self.singleton_stream self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::InventoryManip( .send(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Pickup(uid), InventoryManip::Pickup(uid),
@ -424,6 +443,72 @@ impl Client {
.unwrap(); .unwrap();
} }
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {
self.group_invite
}
pub fn group_info(&self) -> Option<(String, Uid)> {
self.group_leader.map(|l| ("Group".into(), l)) // TODO
}
pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
pub fn send_group_invite(&mut self, invitee: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Invite(invitee),
)))
.unwrap()
}
pub fn accept_group_invite(&mut self) {
// Clear invite
self.group_invite.take();
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Accept,
)))
.unwrap();
}
pub fn decline_group_invite(&mut self) {
// Clear invite
self.group_invite.take();
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Decline,
)))
.unwrap();
}
pub fn leave_group(&mut self) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Leave,
)))
.unwrap();
}
pub fn kick_from_group(&mut self, uid: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::Kick(uid),
)))
.unwrap();
}
pub fn assign_group_leader(&mut self, uid: Uid) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::GroupManip(
GroupManip::AssignLeader(uid),
)))
.unwrap();
}
pub fn is_mounted(&self) -> bool { pub fn is_mounted(&self) -> bool {
self.state self.state
.ecs() .ecs()
@ -433,7 +518,7 @@ impl Client {
} }
pub fn mount(&mut self, entity: EcsEntity) { pub fn mount(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.ecs().read_storage::<Uid>().get(entity).copied() { if let Some(uid) = self.state.read_component_copied(entity) {
self.singleton_stream self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::Mount(uid))) .send(ClientMsg::ControlEvent(ControlEvent::Mount(uid)))
.unwrap(); .unwrap();
@ -690,6 +775,13 @@ impl Client {
frontend_events.append(&mut self.handle_new_messages()?); frontend_events.append(&mut self.handle_new_messages()?);
// 3) Update client local data // 3) Update client local data
// Check if the group invite has timed out and remove if so
if self
.group_invite
.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur)
{
self.group_invite = None;
}
// 4) Tick the client's LocalState // 4) Tick the client's LocalState
self.state.tick(dt, add_foreign_systems, true); self.state.tick(dt, add_foreign_systems, true);
@ -935,7 +1027,102 @@ impl Client {
); );
} }
}, },
ServerMsg::GroupUpdate(change_notification) => {
use comp::group::ChangeNotification::*;
// Note: we use a hashmap since this would not work with entities outside
// the view distance
match change_notification {
Added(uid, role) => {
// Check if this is a newly formed group by looking for absence of
// other non pet group members
if !matches!(role, group::Role::Pet)
&& !self
.group_members
.values()
.any(|r| !matches!(r, group::Role::Pet))
{
frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(
"Type /g or /group to chat with your group members",
)));
}
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!(
"[{}] joined group",
player_info.player_alias
)),
));
}
if self.group_members.insert(uid, role) == Some(role) {
warn!(
"Received msg to add uid {} to the group members but they \
were already there",
uid
);
}
},
Removed(uid) => {
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!(
"[{}] left group",
player_info.player_alias
)),
));
}
if self.group_members.remove(&uid).is_none() {
warn!(
"Received msg to remove uid {} from group members but by they \
weren't in there!",
uid
);
}
},
NewLeader(leader) => {
self.group_leader = Some(leader);
},
NewGroup { leader, members } => {
self.group_leader = Some(leader);
self.group_members = members.into_iter().collect();
// Currently add/remove messages treat client as an implicit member
// of the group whereas this message explicitly includes them so to
// be consistent for now we will remove the client from the
// received hashset
if let Some(uid) = self.uid() {
self.group_members.remove(&uid);
}
},
NoGroup => {
self.group_leader = None;
self.group_members = HashMap::new();
},
}
},
ServerMsg::GroupInvite { inviter, timeout } => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout));
},
ServerMsg::InvitePending(uid) => {
if !self.pending_invites.insert(uid) {
warn!("Received message about pending invite that was already pending");
}
},
ServerMsg::InviteComplete { target, answer } => {
if !self.pending_invites.remove(&target) {
warn!(
"Received completed invite message for invite that was not in the \
list of pending invites"
)
}
// TODO: expose this as a new event variant instead of going
// through the chat
let msg = match answer {
// TODO: say who accepted/declined/timed out the invite
InviteAnswer::Accepted => "Invite accepted",
InviteAnswer::Declined => "Invite declined",
InviteAnswer::TimedOut => "Invite timed out",
};
frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg)));
},
ServerMsg::Ping => { ServerMsg::Ping => {
self.singleton_stream.send(ClientMsg::Pong)?; self.singleton_stream.send(ClientMsg::Pong)?;
}, },
@ -976,7 +1163,7 @@ impl Client {
self.state.ecs_mut().apply_entity_package(entity_package); self.state.ecs_mut().apply_entity_package(entity_package);
}, },
ServerMsg::DeleteEntity(entity) => { ServerMsg::DeleteEntity(entity) => {
if self.state.read_component_cloned::<Uid>(self.entity) != Some(entity) { if self.uid() != Some(entity) {
self.state self.state
.ecs_mut() .ecs_mut()
.delete_entity_and_clear_from_uid_allocator(entity.0); .delete_entity_and_clear_from_uid_allocator(entity.0);
@ -1086,6 +1273,9 @@ impl Client {
/// Get the player's entity. /// Get the player's entity.
pub fn entity(&self) -> EcsEntity { self.entity } pub fn entity(&self) -> EcsEntity { self.entity }
/// Get the player's Uid.
pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity) }
/// Get the client state /// Get the client state
pub fn get_client_state(&self) -> ClientState { self.client_state } pub fn get_client_state(&self) -> ClientState { self.client_state }
@ -1137,7 +1327,7 @@ impl Client {
pub fn is_admin(&self) -> bool { pub fn is_admin(&self) -> bool {
let client_uid = self let client_uid = self
.state .state
.read_component_cloned::<Uid>(self.entity) .read_component_copied::<Uid>(self.entity)
.expect("Client doesn't have a Uid!!!"); .expect("Client doesn't have a Uid!!!");
self.player_list self.player_list
@ -1148,8 +1338,7 @@ impl Client {
/// Clean client ECS state /// Clean client ECS state
fn clean_state(&mut self) { fn clean_state(&mut self) {
let client_uid = self let client_uid = self
.state .uid()
.read_component_cloned::<Uid>(self.entity)
.map(|u| u.into()) .map(|u| u.into())
.expect("Client doesn't have a Uid!!!"); .expect("Client doesn't have a Uid!!!");
@ -1220,7 +1409,7 @@ impl Client {
comp::ChatType::Tell(from, to) => { comp::ChatType::Tell(from, to) => {
let from_alias = alias_of_uid(from); let from_alias = alias_of_uid(from);
let to_alias = alias_of_uid(to); let to_alias = alias_of_uid(to);
if Some(from) == self.state.ecs().read_storage::<Uid>().get(self.entity) { if Some(*from) == self.uid() {
format!("To [{}]: {}", to_alias, message) format!("To [{}]: {}", to_alias, message)
} else { } else {
format!("From [{}]: {}", from_alias, message) format!("From [{}]: {}", from_alias, message)

View File

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

View File

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

View File

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

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

View File

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

528
common/src/comp/group.rs Normal file
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 controller;
mod damage; mod damage;
mod energy; mod energy;
pub mod group;
mod inputs; mod inputs;
mod inventory; mod inventory;
mod last; mod last;
@ -28,13 +29,16 @@ pub use body::{
humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData, humanoid, object, quadruped_low, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
}; };
pub use character_state::{Attacking, CharacterState, StateUpdate}; pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group, SpeechBubble, SpeechBubbleType}; pub use chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
};
pub use controller::{ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip, Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
MountState, Mounting, InventoryManip, MountState, Mounting,
}; };
pub use damage::{Damage, DamageSource}; pub use damage::{Damage, DamageSource};
pub use energy::{Energy, EnergySource}; pub use energy::{Energy, EnergySource};
pub use group::Group;
pub use inputs::CanBuild; pub use inputs::CanBuild;
pub use inventory::{ pub use inventory::{
item, item,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 ids = &state.ids;
let mut events = Vec::new(); let mut events = Vec::new();
@ -186,7 +169,7 @@ impl<'a> Widget for Crafting<'a> {
Text::new(&self.localized_strings.get("hud.crafting")) Text::new(&self.localized_strings.get("hud.crafting"))
.mid_top_with_margin_on(ids.window_frame, 9.0) .mid_top_with_margin_on(ids.window_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(22)) .font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(ids.title_main, ui); .set(ids.title_main, ui);

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -52,6 +52,8 @@ pub struct SessionState {
free_look: bool, free_look: bool,
auto_walk: bool, auto_walk: bool,
is_aiming: bool, is_aiming: bool,
target_entity: Option<specs::Entity>,
selected_entity: Option<(specs::Entity, std::time::Instant)>,
} }
/// Represents an active game session (i.e., the one being played). /// Represents an active game session (i.e., the one being played).
@ -86,6 +88,8 @@ impl SessionState {
free_look: false, free_look: false,
auto_walk: false, auto_walk: false,
is_aiming: false, is_aiming: false,
target_entity: None,
selected_entity: None,
} }
} }
@ -208,18 +212,6 @@ impl PlayState for SessionState {
view_mat, cam_pos, .. view_mat, cam_pos, ..
} = self.scene.camera().dependents(); } = self.scene.camera().dependents();
// Choose a spot above the player's head for item distance checks
let player_pos = match self
.client
.borrow()
.state()
.read_storage::<comp::Pos>()
.get(self.client.borrow().entity())
{
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
_ => cam_pos, // Should never happen, but a safe fallback
};
let (is_aiming, aim_dir_offset) = { let (is_aiming, aim_dir_offset) = {
let client = self.client.borrow(); let client = self.client.borrow();
let is_aiming = client let is_aiming = client
@ -243,30 +235,10 @@ impl PlayState for SessionState {
let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z()); let cam_dir: Vec3<f32> = Vec3::from(view_mat.inverted() * -Vec4::unit_z());
// Check to see whether we're aiming at anything // Check to see whether we're aiming at anything
let (build_pos, select_pos) = { let (build_pos, select_pos, target_entity) =
let client = self.client.borrow(); under_cursor(&self.client.borrow(), cam_pos, cam_dir);
let terrain = client.state().terrain(); // Throw out distance info, it will be useful in the future
self.target_entity = target_entity.map(|x| x.0);
let cam_ray = terrain
.ray(cam_pos, cam_pos + cam_dir * 100.0)
.until(|block| block.is_tangible())
.cast();
let cam_dist = cam_ray.0;
match cam_ray.1 {
Ok(Some(_))
if player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
<= MAX_PICKUP_RANGE_SQR =>
{
(
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)),
)
},
_ => (None, None),
}
};
let can_build = self let can_build = self
.client .client
@ -559,6 +531,24 @@ impl PlayState for SessionState {
let camera = self.scene.camera_mut(); let camera = self.scene.camera_mut();
camera.next_mode(self.client.borrow().is_admin()); camera.next_mode(self.client.borrow().is_admin());
}, },
Event::InputUpdate(GameInput::Select, state) => {
if !state {
self.selected_entity =
self.target_entity.map(|e| (e, std::time::Instant::now()));
}
},
Event::InputUpdate(GameInput::AcceptGroupInvite, true) => {
let mut client = self.client.borrow_mut();
if client.group_invite().is_some() {
client.accept_group_invite();
}
},
Event::InputUpdate(GameInput::DeclineGroupInvite, true) => {
let mut client = self.client.borrow_mut();
if client.group_invite().is_some() {
client.decline_group_invite();
}
},
Event::AnalogGameInput(input) => match input { Event::AnalogGameInput(input) => match input {
AnalogGameInput::MovementX(v) => { AnalogGameInput::MovementX(v) => {
self.key_state.analog_matrix.x = v; self.key_state.analog_matrix.x = v;
@ -708,6 +698,8 @@ impl PlayState for SessionState {
self.scene.camera().get_mode(), self.scene.camera().get_mode(),
camera::CameraMode::FirstPerson camera::CameraMode::FirstPerson
), ),
target_entity: self.target_entity,
selected_entity: self.selected_entity,
}, },
); );
@ -971,6 +963,24 @@ impl PlayState for SessionState {
HudEvent::CraftRecipe(r) => { HudEvent::CraftRecipe(r) => {
self.client.borrow_mut().craft_recipe(&r); self.client.borrow_mut().craft_recipe(&r);
}, },
HudEvent::InviteMember(uid) => {
self.client.borrow_mut().send_group_invite(uid);
},
HudEvent::AcceptInvite => {
self.client.borrow_mut().accept_group_invite();
},
HudEvent::DeclineInvite => {
self.client.borrow_mut().decline_group_invite();
},
HudEvent::KickMember(uid) => {
self.client.borrow_mut().kick_from_group(uid);
},
HudEvent::LeaveGroup => {
self.client.borrow_mut().leave_group();
},
HudEvent::AssignLeader(uid) => {
self.client.borrow_mut().assign_group_leader(uid);
},
} }
} }
@ -979,6 +989,7 @@ impl PlayState for SessionState {
let scene_data = SceneData { let scene_data = SceneData {
state: client.state(), state: client.state(),
player_entity: client.entity(), player_entity: client.entity(),
target_entity: self.target_entity,
loaded_distance: client.loaded_distance(), loaded_distance: client.loaded_distance(),
view_distance: client.view_distance().unwrap_or(1), view_distance: client.view_distance().unwrap_or(1),
tick: client.get_tick(), tick: client.get_tick(),
@ -1033,6 +1044,7 @@ impl PlayState for SessionState {
let scene_data = SceneData { let scene_data = SceneData {
state: client.state(), state: client.state(),
player_entity: client.entity(), player_entity: client.entity(),
target_entity: self.target_entity,
loaded_distance: client.loaded_distance(), loaded_distance: client.loaded_distance(),
view_distance: client.view_distance().unwrap_or(1), view_distance: client.view_distance().unwrap_or(1),
tick: client.get_tick(), tick: client.get_tick(),
@ -1055,3 +1067,108 @@ impl PlayState for SessionState {
self.hud.render(renderer, self.scene.globals()); self.hud.render(renderer, self.scene.globals());
} }
} }
/// Max distance an entity can be "targeted"
const MAX_TARGET_RANGE: f32 = 30.0;
/// Calculate what the cursor is pointing at within the 3d scene
#[allow(clippy::type_complexity)]
fn under_cursor(
client: &Client,
cam_pos: Vec3<f32>,
cam_dir: Vec3<f32>,
) -> (
Option<Vec3<i32>>,
Option<Vec3<i32>>,
Option<(specs::Entity, f32)>,
) {
// Choose a spot above the player's head for item distance checks
let player_entity = client.entity();
let player_pos = match client
.state()
.read_storage::<comp::Pos>()
.get(player_entity)
{
Some(pos) => pos.0 + (Vec3::unit_z() * 2.0),
_ => cam_pos, // Should never happen, but a safe fallback
};
let terrain = client.state().terrain();
let cam_ray = terrain
.ray(cam_pos, cam_pos + cam_dir * 100.0)
.until(|block| block.is_tangible())
.cast();
let cam_dist = cam_ray.0;
// The ray hit something, is it within range?
let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if
player_pos.distance_squared(cam_pos + cam_dir * cam_dist)
<= MAX_PICKUP_RANGE_SQR)
{
(
Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)),
Some((cam_pos + cam_dir * (cam_dist + 0.01)).map(|e| e.floor() as i32)),
)
} else {
(None, None)
};
// See if ray hits entities
// Currently treated as spheres
let ecs = client.state().ecs();
// Don't cast through blocks
// Could check for intersection with entity from last frame to narrow this down
let cast_dist = if let Ok(Some(_)) = cam_ray.1 {
cam_dist.min(MAX_TARGET_RANGE)
} else {
MAX_TARGET_RANGE
};
// Need to raycast by distance to cam
// But also filter out by distance to the player (but this only needs to be done
// on final result)
let mut nearby = (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::Scale>().maybe(),
&ecs.read_storage::<comp::Body>()
)
.join()
.filter(|(e, _, _, _)| *e != player_entity)
.map(|(e, p, s, b)| {
const RADIUS_SCALE: f32 = 3.0;
let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE;
// Move position up from the feet
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
// Distance squared from camera to the entity
let dist_sqr = pos.distance_squared(cam_pos);
(e, pos, radius, dist_sqr)
})
// Roughly filter out entities farther than ray distance
.filter(|(_, _, r, d_sqr)| *d_sqr <= cast_dist.powi(2) + 2.0 * cast_dist * r + r.powi(2))
// Ignore entities intersecting the camera
.filter(|(_, _, r, d_sqr)| *d_sqr > r.powi(2))
// Substract sphere radius from distance to the camera
.map(|(e, p, r, d_sqr)| (e, p, r, d_sqr.sqrt() - r))
.collect::<Vec<_>>();
// Sort by distance
nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
let seg_ray = LineSegment3 {
start: cam_pos,
end: cam_pos + cam_dir * cam_dist,
};
// TODO: fuzzy borders
let target_entity = nearby
.iter()
.map(|(e, p, r, _)| (e, *p, r))
// Find first one that intersects the ray segment
.find(|(_, p, r)| seg_ray.projected_point(*p).distance_squared(*p) < r.powi(2))
.and_then(|(e, p, r)| {
let dist_to_player = p.distance(player_pos);
(dist_to_player - r < MAX_TARGET_RANGE).then_some((*e, dist_to_player))
});
// TODO: consider setting build/select to None when targeting an entity
(build_pos, select_pos, target_entity)
}

View File

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

View File

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

View File

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