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