Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/annezazu/game-mode/releases/download/playground/game-mode.zipChoose a difficulty level for the WordPress Site Editor. Simple locks layout and shows only the basics. Intermediate gives you balanced editing. Advanced unlocks every block-support control for full theme design.
A persistent switcher in the bottom-right of the editor lets you change levels at any time.
Status: experimental. This plugin uses several
__experimentaland__unstableGutenberg APIs and depends on DOM-text matching against editor labels. It may break with Gutenberg releases.
What each level does
| Simple | Intermediate | Advanced | |
|---|---|---|---|
| Patterns | content-only locked | unlocked | unlocked |
| Block-support controls | color + font size only | core defaults | every control expanded by default |
Focus mode (focusMode core preference) |
on | off | off |
| Distraction-free | minimal chrome (helpers off) | normal | normal |
| Theme blocks (Query Loop, Post Title, …) | hidden from inserter | available | available |
| Inserter tabs | Patterns only | all tabs | all tabs |
Choose-pattern modal on new pages (enableChoosePatternModal) |
enabled | enabled | disabled |
| Block-directory installs | disabled | disabled | enabled |
| RichText toolbar formats | bold / italic / underline / strike / link / inline-code / text-color | full | full |
Code Editor (codeEditingEnabled) |
disabled | disabled | enabled |
| Styles sidebar | Browse styles + Colors + Typography + Background only | full | full |
| Pattern editing UX | locked unless "Edit pattern" clicked | auto-unlocked | auto-unlocked |
| Lock-removal on root blocks | locked (Backspace can't wipe a section) | unlocked | unlocked |
Try it in WordPress Playground
No install needed — open the latest build in a throwaway WordPress site in your browser:
▶ Launch Game Mode in Playground
You'll be auto-logged-in as admin and dropped straight into the Site Editor with the plugin active. Each push to main triggers a rebuild, so the link always reflects the latest code.
Install
This repo ships source only. To use the plugin you need to build it once, then drop the directory into wp-content/plugins/.
git clone https://github.com/annezazu/game-mode.git
cd game-mode
npm install
npm run build
Then either copy the directory into your WordPress install:
cp -R . /path/to/wordpress/wp-content/plugins/game-mode
…or symlink it:
ln -s "$(pwd)" /path/to/wordpress/wp-content/plugins/game-mode
Activate Game Mode in Plugins → Installed Plugins and open the Site Editor. The first time it runs you'll see a "Choose your difficulty" modal.
Local development with wp-env
npm run start # webpack watch
npx wp-env start # boots a local WP at http://localhost:8898
npx wp-env run cli wp core version
The bundled .wp-env.json pins WordPress to a recent release and mounts this directory as a plugin via a no-space symlink at /tmp/game-mode-link/game-mode (wp-env tokenizes plugin paths on whitespace).
Tests
npm test
Jest unit tests cover the pure block-support helpers (minimizeSupports, expandDefaultControls).
Reproducing first-run for testing
The "Choose your difficulty" picker fires once, when all of the following are true: no game_mode_level user meta, no core/preferences['game-mode']['level'], and the Site Editor's welcome guide has been dismissed. To force a fresh first-run experience without uninstalling/reinstalling, run this in the browser console while on site-editor.php:
// 1. Clear the per-user meta (level lives on /wp/v2/users/me).
await wp.apiFetch( {
path: '/wp/v2/users/me',
method: 'POST',
data: { meta: { game_mode_level: '' } },
} );
// 2. Clear the in-store preference.
wp.data.dispatch( 'core/preferences' ).set( 'game-mode', 'level', undefined );
// 3. Dismiss the Site Editor welcome guide so it doesn't block our picker.
wp.data.dispatch( 'core/preferences' ).set( 'core/edit-site', 'welcomeGuide', false );
location.reload();
After reload, the picker should appear. If it doesn't, check wp.data.select('core').getCurrentUser().meta.game_mode_level — it should be empty.
Architecture
- PHP (
game-mode.php): bootstrap, asset enqueue gated tosite-editor.php, registered user meta (game_mode_level) exposed through/wp/v2/users/me, and three server-side curation layers —allowed_block_types_allfor theme-block hiding on Simple,block_editor_settings_allfor per-level editor settings (codeEditingEnabled,disableContentOnlyForUnsyncedPatterns), andwp_theme_json_data_userfor Simple's declarative settings constraints (appearance-tools off, advanced typography keys off, etc.). The theme.json layer is belt-and-braces on top ofminimizeSupports— settings can't gate the ToolsPanel "more" menu, only the default-visible set. - JS bundle (
src/): React UI for the picker modal + bottom-right switcher (@wordpress/components), level configuration (levels.js), and a stack ofMutationObserver/subscribe()-driven filters insrc/filters/that hide-or-modify parts of the editor based on the active level.
Stability — experimental Gutenberg APIs in use
This plugin reaches into a number of __experimental* and __unstable* APIs from the Gutenberg packages. Those carry no compatibility guarantee — they can be renamed or removed in any release without a deprecation cycle. The list below is the surface area to watch when bumping @wordpress/* deps:
| API | File | What breaks if it goes away |
|---|---|---|
__experimentalGetDirtyEntityRecords (core store) |
src/index.js |
Unsaved-changes guard before reload — falls back to no warning |
__experimentalConfirmDialog (@wordpress/components) |
src/index.js |
The "Save before switching?" dialog stops rendering |
__unstableMotion / __unstableAnimatePresence (@wordpress/components) |
src/components/LevelSwitcher.jsx |
Switcher icon swap goes from animated → instant |
__unstableMarkNextChangeAsNotPersistent (core/block-editor dispatch) |
src/filters/easy-lock-remove.js |
Lock-remove writes get marked as user edits, dirtying the post |
__experimentalGetAllowedPatterns (core/block-editor selector) |
n/a directly today, but used by patterns observer | Pattern list filtering can no-op |
__experimentalText / __experimentalHeading / __experimentalVStack / __experimentalHStack (@wordpress/components) |
LevelCard, LevelPickerModal |
Build error — these are not optional, swap with stable Text/Heading/Flex if removed |
In addition, several filters depend on DOM-text matching against editor labels (e.g. matching panel headers by their visible "Layout" / "Edit pattern" text). Those break in non-English locales and on label changes:
src/filters/easy-block-inspector.js— "Edit pattern", "Styles", "Layout", "Dimensions", "Border", "Position", "Shadow", "Background", "Advanced"src/filters/easy-styles-menu.js— "Browse styles", "Colors", "Typography", "Background", + the "customize the appearance of specific blocks" descriptionsrc/filters/easy-patterns-only-inserter.js— "Patterns"
Also worth noting: showSimpleTopbar and showBlockHelpers in src/levels.js mirror an unmerged Gutenberg PR (#74546). On stock WordPress they are no-ops; the visual effect is provided by the CSS in src/filters/distraction-free-config.js.
Known limitations
Root-block lock attributes persist after an explicit save on Simple. src/filters/easy-lock-remove.js writes lock.remove: true to root blocks while on Simple so Backspace can't wipe a whole section. The write is marked non-persistent via __unstableMarkNextChangeAsNotPersistent, but if the user explicitly saves the template/page while on Simple, the attribute goes along for the ride into the saved post_content. After switching to Intermediate or Advanced, those blocks still render with the lock icon until they're re-saved at the higher level.
Workaround: open the affected block on Advanced, click the toolbar lock icon to unlock, and save. Or wait until you next edit and re-save the template — easy-lock-remove's cleanup pass strips the attribute in memory on level switch, so the next save will write a clean copy.
This isn't fixed in code because every clean fix path (filtering on render, rewriting on save, intercepting canRemoveBlock) carries enough Core-coupling and edge-case risk to outweigh the cost of a niche visual nuisance.
License
GPL-2.0-or-later.
Releases
1 release. Each count is every asset in that release; expand a row for the breakdown.