diff --git a/core/client/ui/toolboxes/level-toolbox.hbs.html b/core/client/ui/toolboxes/level-toolbox.hbs.html
index c6e1898f2..487c9cb94 100644
--- a/core/client/ui/toolboxes/level-toolbox.hbs.html
+++ b/core/client/ui/toolboxes/level-toolbox.hbs.html
@@ -9,6 +9,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/client/ui/toolboxes/level-toolbox.js b/core/client/ui/toolboxes/level-toolbox.js
index a4ca28adb..62e4ae779 100644
--- a/core/client/ui/toolboxes/level-toolbox.js
+++ b/core/client/ui/toolboxes/level-toolbox.js
@@ -6,7 +6,7 @@ const checkLevelName = value => {
if (value.length < 3) throw new Error('Level\'s name must be at least 2 characters');
};
-const updateLevel = (name, spawnPosition, hide = false) => {
+const updateLevel = (name, spawnPosition, hide = false, featuresPermissions) => {
try {
checkLevelName(name);
} catch (e) {
@@ -14,23 +14,23 @@ const updateLevel = (name, spawnPosition, hide = false) => {
return;
}
- Meteor.call('updateLevel', name, spawnPosition, hide, err => {
+ Meteor.call('updateLevel', name, spawnPosition, hide, featuresPermissions, err => {
if (err) { lp.notif.error(err.reason); return; }
lp.notif.success('Level updated!');
});
};
+const getFeaturesPermissions = () => currentLevel(Meteor.user()).featuresPermissions || {};
+
Template.levelToolbox.events({
'focus input'() { toggleUIInputs(true); },
'blur input'() { toggleUIInputs(false); },
'blur .js-name'(event) {
- const user = Meteor.user();
- const level = currentLevel(user);
+ const level = currentLevel(Meteor.user());
updateLevel(event.target.value, level.spawn, level.hide);
},
'change .js-hidden'(event) {
- const user = Meteor.user();
- const level = currentLevel(user);
+ const level = currentLevel(Meteor.user());
updateLevel(level.name, level.spawn, event.target.checked);
},
'click .js-spawn-position'() {
@@ -39,6 +39,20 @@ Template.levelToolbox.events({
const { x, y } = user.profile;
updateLevel(level.name, { x, y }, level.hide);
},
+ 'change .js-voice-amplifier-select'(event) {
+ const level = currentLevel(Meteor.user());
+
+ updateLevel(level.name, level.spawn, level.hide, { shout: event.target.value });
+ },
+ 'change .js-global-chat-select'(event) {
+ const level = currentLevel(Meteor.user());
+ updateLevel(level.name, level.spawn, level.hide, { globalChat: event.target.value });
+ },
+ 'change .js-punch-select'(event) {
+ const level = currentLevel(Meteor.user());
+
+ updateLevel(level.name, level.spawn, level.hide, { punch: event.target.value });
+ },
});
Template.levelToolbox.helpers({
@@ -48,4 +62,14 @@ Template.levelToolbox.helpers({
const { spawn } = currentLevel(Meteor.user());
return `${Math.round(spawn.x)} - ${Math.round(spawn.y)}`;
},
+ dropdownValues() {
+ return [
+ { value: 'enabled', label: 'Enabled' },
+ { value: 'adminOnly', label: 'Admin only' },
+ { value: 'disabled', label: 'Disabled' },
+ ];
+ },
+ shout() { return getFeaturesPermissions().shout || 'enabled'; },
+ globalChat() { return getFeaturesPermissions().globalChat || 'enabled'; },
+ punch() { return getFeaturesPermissions().punch || 'enabled'; },
});
diff --git a/core/client/ui/toolboxes/level-toolbox.scss b/core/client/ui/toolboxes/level-toolbox.scss
index da55cc696..b300a3a82 100644
--- a/core/client/ui/toolboxes/level-toolbox.scss
+++ b/core/client/ui/toolboxes/level-toolbox.scss
@@ -10,4 +10,15 @@
font-size: 0.85rem;
margin: 15px 0;
}
+
+ .feature-dropdown {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .select {
+ width: fit-content;
+ min-width: 110px;
+ }
}
diff --git a/core/lib/misc.js b/core/lib/misc.js
index 40a64b41e..0a0808f51 100644
--- a/core/lib/misc.js
+++ b/core/lib/misc.js
@@ -235,14 +235,30 @@ const teleportUserInLevel = (user, level, source = 'teleporter') => {
return level.name;
};
+const canUseLevelFeature = (user, featureName) => {
+ check(user._id, Match.Id);
+ check(featureName, String);
+
+ const level = currentLevel(user);
+ const featurePermission = level?.featuresPermissions?.[featureName];
+
+ if (featurePermission === 'disabled') {
+ if (user.roles?.admin) lp.notif.error(`This feature is disabled: ${featureName}`);
+ return false;
+ } else if (!user.roles?.admin && featurePermission === 'adminOnly') {
+ return false;
+ } else return true;
+};
+
export {
canAccessZone,
- canEditGuild,
canEditActiveLevel,
+ canEditGuild,
canEditLevel,
canEditUserPermissions,
canModerateLevel,
canModerateUser,
+ canUseLevelFeature,
completeUserProfile,
currentLevel,
fileOnBeforeUpload,
diff --git a/core/modules/console/client/console.hbs.html b/core/modules/console/client/console.hbs.html
index 3c80dac9d..7da62ea18 100644
--- a/core/modules/console/client/console.hbs.html
+++ b/core/modules/console/client/console.hbs.html
@@ -1,10 +1,12 @@
diff --git a/core/modules/console/client/console.js b/core/modules/console/client/console.js
index 488343090..c67c94a06 100644
--- a/core/modules/console/client/console.js
+++ b/core/modules/console/client/console.js
@@ -1,4 +1,5 @@
import { toggleUIInputs } from '../../../client/helpers';
+import { canUseLevelFeature, currentLevel } from '../../../lib/misc';
const inputSelector = '.console .js-command-input';
const inputFileSelector = '.console .console-file';
@@ -140,3 +141,17 @@ Template.console.events({
onSubmit();
},
});
+
+Template.console.helpers({
+ chatDisabled: () => {
+ const channel = Session.get('messagesChannel');
+ const user = Meteor.user({ fields: { _id: 1, 'profile.levelId': 1, roles: 1 } });
+
+ if (!user) return;
+
+ const level = currentLevel(user);
+
+ if (channel === level?._id) return !canUseLevelFeature(user, 'globalChat');
+ return false;
+ },
+});
diff --git a/core/modules/console/client/console.scss b/core/modules/console/client/console.scss
index c12a7828c..c7409ce91 100644
--- a/core/modules/console/client/console.scss
+++ b/core/modules/console/client/console.scss
@@ -38,6 +38,12 @@ $button-text-color: white;
height: 50px;
max-height: 200px;
min-height: 50px;
+
+ &.disabled {
+ background-color: $new-dark-secondary;
+ padding-left: 20px;
+ font-style: italic;
+ }
}
}
diff --git a/core/modules/punch-ability/client/punch-ability.js b/core/modules/punch-ability/client/punch-ability.js
index 12546229d..236b6d01a 100644
--- a/core/modules/punch-ability/client/punch-ability.js
+++ b/core/modules/punch-ability/client/punch-ability.js
@@ -1,4 +1,5 @@
import audioManager from '../../../client/audio-manager';
+import { canUseLevelFeature } from '../../../lib/misc';
const playPunchAnimation = () => {
userManager.scene.cameras.main.shake(250, 0.015, 0.02);
@@ -26,6 +27,9 @@ window.addEventListener('load', () => {
});
hotkeys('x', { scope: scopes.player }, e => {
+ const user = Meteor.user({ fields: { _id: 1, 'profile.levelId': 1, roles: 1 } });
+ if (!user || !canUseLevelFeature(user, 'punch')) return;
+
e.preventDefault();
e.stopPropagation();
if (e.repeat) return;
diff --git a/core/modules/shout-ability/client/shout-ability.js b/core/modules/shout-ability/client/shout-ability.js
index 6b746163c..75f80f042 100644
--- a/core/modules/shout-ability/client/shout-ability.js
+++ b/core/modules/shout-ability/client/shout-ability.js
@@ -1,15 +1,36 @@
+import { canUseLevelFeature } from '../../../lib/misc';
+
+
window.addEventListener('load', () => {
- registerRadialMenuModules([
- { id: 'shout', icon: '📢', label: 'Shout', order: 40, shortcut: 55, scope: 'me' },
- ]);
+ Tracker.nonreactive(() => {
+ const user = Meteor.user();
+
+ if (!user) return;
+
+ const isAdmin = user.roles?.admin;
+ const isShoutFeatureEnabled = canUseLevelFeature(Meteor.user(), 'shout');
+
+ if (isAdmin || isShoutFeatureEnabled) {
+ registerRadialMenuModules([
+ { id: 'shout', icon: '📢', label: 'Shout', order: 40, shortcut: 55, scope: 'me' },
+ ]);
+ }
+ });
hotkeys('r', { keyup: true, scope: scopes.player }, event => {
if (event.repeat) return;
+
+ const user = Meteor.user({ fields: { _id: 1, 'profile.levelId': 1, roles: 1 } });
+
+ if (!user || !canUseLevelFeature(user, 'shout')) return;
+
userVoiceRecorderAbility.recordVoice(event.type === 'keydown', sendAudioChunksToUsersInZone);
});
const onMenuOptionSelected = e => {
const { option } = e.detail;
+ const user = Meteor.user({ fields: { _id: 1, 'profile.levelId': 1, roles: 1 } });
+
if (option.id !== 'shout') return;
userVoiceRecorderAbility.recordVoice(true, sendAudioChunksToUsersInZone);
@@ -17,6 +38,8 @@ window.addEventListener('load', () => {
const onMenuOptionUnselected = e => {
const { option } = e.detail;
+ const user = Meteor.user({ fields: { _id: 1, 'profile.levelId': 1, roles: 1 } });
+
if (option.id !== 'shout') return;
userVoiceRecorderAbility.recordVoice(false, sendAudioChunksToUsersInZone);
diff --git a/core/server/levels.js b/core/server/levels.js
index 5dbe67641..3ae625439 100644
--- a/core/server/levels.js
+++ b/core/server/levels.js
@@ -189,7 +189,7 @@ Meteor.publish('currentLevel', function () {
return Levels.find(
{ _id: levelId },
- { fields: { name: 1, spawn: 1, hide: 1, height: 1, width: 1, editorUserIds: 1, createdBy: 1, sandbox: 1, guildId: 1 } },
+ { fields: { name: 1, spawn: 1, hide: 1, height: 1, width: 1, editorUserIds: 1, createdBy: 1, sandbox: 1, guildId: 1, featuresPermissions: 1 } },
);
});
@@ -211,11 +211,12 @@ Meteor.methods({
return createLevel({ templateId });
},
- updateLevel(name, position, hide) {
+ updateLevel(name, position, hide, featurePermission = null) {
if (!this.userId) throw new Meteor.Error('missing-user', 'A valid user is required');
check(name, String);
check(position, { x: Number, y: Number });
check(hide, Boolean);
+ check(featurePermission, Match.OneOf(null, { shout: String }, { globalChat: String }, { punch: String }));
const user = Meteor.user();
const level = currentLevel(Meteor.user());
@@ -225,9 +226,11 @@ Meteor.methods({
const query = { $set: { name, spawn: { x: position.x, y: position.y } } };
if (hide) query.$set.hide = true;
else query.$unset = { hide: 1 };
+ if (featurePermission) query.$set.featuresPermissions = { ...level.featuresPermissions || {}, ...featurePermission };
Levels.update(level._id, query);
},
+
increaseLevelVisits(levelId) {
if (!this.userId) return;
check(levelId, Match.Id);