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);