---
title: "What the Footer Game | Footer Arcade for YOOtheme Pro"
description: "Turn your YOOtheme Pro footer into an arcade with 9 mini games, highscores, rewards, leaderboards, challenges and QR sharing for fun and engagement online."
url: "https://www.yoozecase.com/?app_id=1&item_id=12"
date: "2026-10-08T08:25:32+00:00"
language: "en-GB"
---

#  What the F...ooter? ...game!

Nobody scrolls that far. Until now.

**Latest Version**
0.18.0
**Requires**
Yootheme Pro 5+, Joomla 5+, WordPress 6+, PHP 8.1 or newer
**Release date**
05 October 2026
**Made for**
Yootheme Pro, Joomla, Wordpress
**License**
GPL-2.0-or-later
**Price**
€25.00 (incl. 6 months of updates)

 [ 🛒 Get it for Joomla 👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/d6b1e35e-c12f-427d-a85d-6a16ce7e69c8)

 [ 🛒 Get it for WordPress👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/821489a8-98b3-4f49-b48a-6a65a1c83f44)

 [ Demo page ](https://www.yoozecase.com/what-the-footer-game-demo)

Hide a tiny game in your footer. Nobody scrolls that far .... until now!Turn your Joomla footer into a playable arcade. What the F...ooter? ...game! adds a YOOtheme Pro element with 9 built-in games, live highscores, unlockable rewards and an opt-in global leaderboard, a fun, sticky reason for visitors to stay a little longer.

##  What's new in version 0.18.0

- **Security:** Player names that a visitor typed as encoded HTML (for example &lt;b&gt;Zoe&lt;/b&gt;) were saved with the HTML tags in them. The game, the highscore boards, the dashboard and the e-mails always show names as plain text, so the tags never ran on your site. Names are now cleaned before they are saved, and the update cleans names that were saved earlier.
- **Security:** The license status can only be read by administrators who may manage plugins.
- **Changed:** Updates now need an active license key. From the next release on, Joomla installs an update of this package only while a key is activated on the License tab; without one, System > Update Sites shows "The Download Key is missing". This release itself still installs without a key. The game and all settings keep working with or without a key.
- **Changed:** The license is checked once a day while an administrator uses the back end, so a renewed or expired key is picked up without pressing a button. When the license server cannot be reached nothing changes.
- **Changed:** The plugin, the dashboard module and the quick icon now carry the package version number.
- **Changed:** The update site is called "What the F...ooter? ...game! Updates", and the package is listed as "What the F...ooter? ...game!".
- **Changed:** This changelog is in English and lists package versions.
- **Fixed:** LICENSE.txt named a folder instead of the product.

 ![What The Footer Game 1](https://www.yoozecase.com/files/what-the-footer-game_1.png)

 [](#)

 [](#)

---

##  Description

Give your footer a reason to be scrolled to.*What the F...ooter? ...game!* is a YOOtheme Pro element for Joomla 5 &amp, 6 that hides a full arcade inside your footer. A play button slides your footer content aside and drops the visitor straight into a game, then slides back when they're done. It's built to be genuinely fun, on-brand, and impossible to resist a "one more try."

Nine games, one element.
Pick from Space Shooter, Endless Runner, Pong, Brick Breaker, Snake, Footer Flyer, Meteor Dodge, Whack-a-Mole and 2048, each with its own highscore board.

Highscores that keep people coming back.
Live per-game leaderboards with all-time / monthly / weekly / daily views, plus an optional ****seeded Daily Challenge
where everyone plays the exact same layout that day. Turn on the **global, cross-site leaderboard
and scores from every site running the plugin compete on one board with a separate, drop-anywhere **Global Highscores
element (retro-pixel, neon, podium and terminal themes included).********

********Reward your champions.
Automatically unlock a code, a message and/or a downloadable file when a player beats the top score with optional, consent-based email capture so you can follow up with your best players.********

********Make it yours.
Use your logo as the player, choose colour presets or a picker, enable seasonal colours, retro CRT and arcade sound effects, floating power-ups, and a hidden Konami easter egg. It's fully responsive (the scene drops below the text on mobile), theme-neutral, privacy-conscious, and translated into six languages.********

********Built-in virality.
Players can share their score, download a score-card image with a QR code, and hand their phone a scan-to-play QR. Every game becomes a tiny invitation to the next player.********

Plus an admin dashboard with moderation, a profanity filter, analytics and a control-panel quick-icon. It's the friendliest way to turn dead footer space into engagement.

##  Feature request

 ### What is the next feature for this plugin?

Submit your idea or vote on existing ones.

  #### Top voted

   Website (leave empty)

'; } html += ''; if (oc.title) { html += '##### ' + escapeHtml(oc.title) + '

'; } if (oc.text) { html += '' + escapeHtml(oc.text).replace(/\\n/g, '
') + '

'; } html += '

'; } // Auto winner(s) - top N from the already vote-sorted features if (mode === 'auto_winner' || mode === 'both') { var n = oc.winnerCount | 0; if (n '; for (var i = 0; i ' + (w.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(w.image_url)%20+%20')

' : '') + '' + (i + 1) + '' + '' + escapeHtml(w.title) + '' + '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' + ''; } html += ''; } } if (html === '') { this.elOutcome.hidden = true; return; } this.elOutcome.innerHTML = html; this.elOutcome.hidden = false; }; /** * Roadmap view (v3.2.0): renders the items as status columns, in the * canonical status order from cfg.statuses. Each column is an 12. inside \* the existing .yc-fb-list , holding its own inner  of items - \* this keeps the delegated click/change handlers on elList working \* unchanged. Unknown statuses fall back into the "open" column so no item \* can silently disappear. Empty columns are skipped when \* cfg.roadmapHideEmpty is set. \*/ Board.prototype.renderRoadmap = function (items) { var stats = (this.cfg.statuses && this.cfg.statuses.length) ? this.cfg.statuses : \['open', 'planned', 'in\_progress', 'shipped', 'declined'\]; var labels = this.cfg.statusLabels || {}; var groups = {}; var i, st; for (i = 0; i ' + '##### ' + '' + escapeHtml(labels\[sv\] || sv) + '' + arr.length + '' + '

    ' + '' + itemsHtml + '
    ' + '
'; } return html; }; Board.prototype.renderItem = function (f, index) { var disabled = this.cfg.isClosed ? 'disabled' : ''; var voted = f.voted ? ' yc-voted' : ''; var descHtml = f.description ? '' + escapeHtml(f.description).replace(/\\n/g, '
') + '

' : ''; var thumbHtml = f.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

' : ''; // Status workflow (v3.0.0): badge for any non-default status. The // roadmap view (v3.2.0) skips the badge - the column already // communicates the status, a badge would be redundant noise. var st = (f.status || 'open'); var labels = this.cfg.statusLabels || {}; var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st]) ? '' + escapeHtml(labels\[st\]) + '' : ''; // Design themes (v3.1.0): arena / neon / pixel render extra per-item // furniture - rank number, avatar (image or initials), a "leader" pill // on rank 1, and a "votes" label. Classic/colour themes are untouched. var t = this.cfg.theme || 'light'; var themed = (t === 'arena' || t === 'neon' || t === 'pixel'); var rank = (typeof index === 'number') ? (index + 1) : 0; var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = ''; if (themed) { rankHtml = '' + pad2(rank) + ''; if (rank === 1) { leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + ''; } if (f.image_url) { avatarHtml = '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

'; } else { avatarHtml = '' + escapeHtml(initialsOf(f.title)) + '

'; } votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + ''; } // Moderation overlay: subtle corner buttons, only when canModerate. // Buttons appear on hover/focus (styled in CSS). Even in customizer // preview we render them when canModerate is true so the moderator // can see what visitors with moderation rights will see. var modHtml = ''; if (this.cfg.canModerate) { var parts = []; var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature')); var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature')); if (this.cfg.allowSoftHide) { parts.push( '' ); } if (this.cfg.allowHardDelete) { parts.push( '' ); } if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) { var sParts = parts.slice(); if (this.cfg.statuses && this.cfg.statuses.length) { var opts = ''; for (var si = 0; si ' + escapeHtml(sl) + ''; } var setStatus = escapeHtml(this.t('setStatus', 'Set status')); sParts.push( '' ); } modHtml = '' + sParts.join('') + '

'; } } var mediaHtml = themed ? avatarHtml : thumbHtml; return ( '32. ' + leaderHtml + rankHtml + '' + mediaHtml + '' + '##### ' + escapeHtml(f.title) + '

    ' + statusBadge + descHtml + '

    ' + modHtml + '
' ); }; /** * Detects whether a JSON response looks like a CSRF/token rejection. * Two cases: * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top * level message contains the JINVALID_TOKEN_NOTICE text and HTTP * status is typically 403. * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token". */ Board.prototype.isTokenError = function (status, raw, payload) { if (status === 403) return true; var hay = ''; try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {} return /invalid (security )?token/i.test(hay); }; /** * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so * the next call (and any future calls in this Board's lifetime) use the * correct one. Cache-bust with a timestamp so no upstream CDN caches the * response. */ Board.prototype.fetchFreshToken = function () { var self = this; var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now(); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }) .then(function (r) { return r.json(); }) .then(function (data) { var p = self.unwrapAjax(data); if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) { self.cfg.csrfToken = p.token; return p.token; } throw new Error('No fresh token in response'); }); }; /** * Generic POST-with-token helper. Builds FormData from a plain object, * adds the current token, posts. On 403 / token-error response, fetches a * fresh token and retries exactly once. * * @param {string} task The task name (appended to URL and body) * @param {object} fields Key-value form fields (no token) * @return {Promise} Resolves to the unwrapped payload on success. \* Rejects with Error containing .reason on failure. \*/ Board.prototype.postWithToken = function (task, fields) { var self = this; function attempt(token) { var fd = new FormData(); fd.append('task', task); for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) { fd.append(k, fields\[k\]); } } fd.append(token, '1'); return fetch(self.cfg.ajaxUrl + '&task=' + encodeURIComponent(task), { method: 'POST', credentials: 'same-origin', body: fd, headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json().then(function (data) { return { status: r.status, raw: data, payload: self.unwrapAjax(data) }; }); }); } return attempt(this.cfg.csrfToken).then(function (res) { // Success path if (res.payload && res.payload.ok === true) { return res.payload; } // Token rejection? Fetch fresh and retry once. if (self.isTokenError(res.status, res.raw, res.payload)) { return self.fetchFreshToken().then(function (fresh) { return attempt(fresh).then(function (res2) { if (res2.payload && res2.payload.ok === true) { return res2.payload; } var err = new Error( (res2.payload && res2.payload.error) || (res2.raw && res2.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res2.payload || null; throw err; }); }); } // Some other error - surface it var err = new Error( (res.payload && res.payload.error) || (res.raw && res.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res.payload || null; throw err; }); }; Board.prototype.handleSubmit = function () { var self = this; if (isInBuilder()) { this.showStatus(this.t('previewSubmit', 'Preview mode - submissions are disabled in the YOOtheme builder. Try this on the published page.'), 'info'); return; } if (this.cfg.isClosed) { this.showStatus(this.cfg.closedMessage || this.t('votingClosed', 'Voting is closed.'), 'info'); return; } var title = (this.elTitle && this.elTitle.value || '').trim(); var desc = (this.elDesc && this.elDesc.value || '').trim(); // Client-side validation: minimum title length. Server re-validates. var minLen = (this.cfg.minTitleLength | 0) || 2; if (title.length for hide/delete, remember parent + sibling for rollback. var li = btn.closest('li'); var parentEl = li ? li.parentNode : null; var nextEl = li ? li.nextSibling : null; if (li && (action === 'hide' || action === 'delete')) { li.remove(); } btn.disabled = true; var task = action === 'hide' ? 'moderate\_hide' : action === 'delete' ? 'moderate\_delete' : action === 'restore' ? 'moderate\_restore' : ''; if (!task) { btn.disabled = false; return; } this.postWithToken(task, { feature\_id: featureId }) .then(function (payload) { // Success - keep the UI as-is (already removed) and refresh // so vote counts, top box, and hidden-features panel reflect // the new state. self.refresh(); if (self.hiddenOpen) { self.hiddenLoaded = false; // force a refetch self.fetchHiddenFeatures(); } var msg = action === 'delete' ? self.t('deleted', 'Feature deleted.') : action === 'hide' ? self.t('hidden', 'Feature hidden.') : action === 'restore' ? self.t('restored', 'Feature restored.') : self.t('done', 'Done.'); self.showStatus(msg, 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { // Rollback: re-insert the row if we removed it. if (li && parentEl) { if (nextEl) { parentEl.insertBefore(li, nextEl); } else { parentEl.appendChild(li); } } btn.disabled = false; self.showStatus(err && err.message ? err.message : self.t('modFailed', 'Moderation action failed.'), 'error'); }); }; Board.prototype.handleStatusChange = function (featureId, status, sel) { var self = this; if (!featureId || !status) return; sel.disabled = true; this.postWithToken('moderate\_status', { feature\_id: featureId, status: status }) .then(function () { self.refresh(); var label = (self.cfg.statusLabels && self.cfg.statusLabels\[status\]) ? self.cfg.statusLabels\[status\] : status; self.showStatus(self.tf('statusSet', 'Status: %s', label), 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { sel.disabled = false; self.showStatus(err && err.message ? err.message : self.t('statusFailed', 'Could not set status.'), 'error'); }); }; /\*\* \* Toggle the hidden-features panel open/closed. First open triggers \* an AJAX fetch; subsequent toggles just hide/show the cached list. \*/ Board.prototype.toggleHiddenPanel = function () { if (!this.elHiddenList || !this.elHiddenLabel) return; var arrow = this.elHiddenToggle && this.elHiddenToggle.querySelector('.yc-fb-hidden-arrow'); if (this.hiddenOpen) { this.hiddenOpen = false; this.elHiddenList.hidden = true; this.elHiddenLabel.textContent = this.t('showHidden', 'Show hidden features'); if (arrow) arrow.classList.remove('yc-open'); return; } this.hiddenOpen = true; this.elHiddenList.hidden = false; this.elHiddenLabel.textContent = this.t('hideHidden', 'Hide hidden features'); if (arrow) arrow.classList.add('yc-open'); if (!this.hiddenLoaded) { this.fetchHiddenFeatures(); } }; /\*\* \* Fetch the hidden-features list via the moderator-only endpoint. \* Re-callable to refresh the list after a restore. \*/ Board.prototype.fetchHiddenFeatures = function () { var self = this; if (!this.elHiddenList) return; this.elHiddenList.innerHTML = '2. ' + escapeHtml(this.t('loading', 'Loading...')) + '
'; var url = this.cfg.ajaxUrl + '&task=moderate\_list\_hidden&board\_id=' + encodeURIComponent(this.cfg.boardId); fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.json(); }) .then(function (data) { var payload = self.unwrapAjax(data); if (!payload || payload.ok !== true) { self.elHiddenList.innerHTML = '4. ' + escapeHtml((payload && payload.error) ? payload.error : self.t('hiddenLoadFailed', 'Failed to load hidden features.')) + '
'; return; } self.hiddenLoaded = true; var features = payload.features || \[\]; if (features.length === 0) { self.elHiddenList.innerHTML = '6. ' + escapeHtml(self.t('noHidden', 'No hidden features.')) + '
'; return; } var html = ''; for (var i = 0; i ' + escapeHtml(f.description).replace(/\\n/g, '
') + '' : ''; var restoreTitle = escapeHtml(self.t('restoreTitle', 'Restore this feature')); html += '9. ' + '' + '##### ' + escapeHtml(f.title) + '

    ' + descHtml + '' + escapeHtml(self.tf('votesMeta', '%s votes', f.votes | 0)) + '' + '

    ' + '' + '
'; } self.elHiddenList.innerHTML = html; }) .catch(function () { self.elHiddenList.innerHTML = '11. ' + escapeHtml(self.t('hiddenNetwork', 'Network error while loading hidden features.')) + '
'; }); }; function initAll(scope) { var roots = (scope || document).querySelectorAll('.yc-fb-root'); for (var i = 0; i ---

##  Changelog

---

 [ v0.8.0 ](#) Added- \*\*Resetting leaderboards\*\*: het highscorebord kan een tijdvenster tonen -

- Server: `scores\_get`/`score\_submit` nemen een `period`-param; nieuwe
- 12 nieuwe taalsleutels x 6 (204 per taal, pariteit).

 Changed- New version released.

 [ v0.7.2 ](#) Changed- \*\*Profanity-vervanging kiest nu een WILLEKEURIGE naam\*\* i.p.v. een vaste. Het

- `profanity\_fallback`-veld: `text` -> `textarea`; default `PLAYER` verwijderd
- Geen DB/schema. Over v0.7.x installeren. Wie eerder een vaste vervangnaam had

 [ v0.7.1 ](#) Fixed- Admin-dashboard onleesbaar in de \*\*dark mode\*\* van de Joomla-backend: de CSS

 [ v0.7.0 ](#) Added- \*\*Owner dashboard\*\* op de plugin-instellingenpagina (custom form field,

- \*\*Moderatie\*\* (super-admin only, CSRF-getokend via com\_ajax): een losse score
- \*\*Profanity-filter\*\* (Moderation-tab, standaard aan): namen met geblokkeerde
- \*\*Analytics-events\*\* (Analytics-tab, standaard uit): vuurt een game-start- en
- 35 nieuwe taalsleutels x 6 talen (192 per taal, pariteit), incl. een

 Changed- Manifest `&lt;config&gt;` heropgebouwd: tabs Dashboard / Moderation / Analytics /
- `NOTICE.txt` + `README.md`: data-processing-notice uitgebreid (analytics
- Geen DB/schema. Over v0.6.x installeren (`method="upgrade"`).
- Volledig. Profanity-filter staat aan (vervangt alleen scheldnamen); analytics

 [ v0.6.0 ](#) Added- \*\*Twee nieuwe games\*\* via het Game-veld (pluggable modules; erven automatisch

- \*\*Snake\*\* (`snake`): klassieke grid-snake; eet voer om te groeien, eindeloos
- \*\*Flyer\*\* (`flyer`): one-button flapper; tik/spatie om te fladderen door de
- 7 nieuwe taalsleutels x 6 talen (157 per taal, pariteit), incl. de 2

 Changed- Game-keuze (element.php select) + `gameType`-whitelist + standaardtitels +
- Geen DB/schema. Over v0.5.x installeren (`method="upgrade"`).
- Volledig. Bestaande elementen blijven op hun gekozen game staan.

 [ v0.5.1 ](#) Added- \*\*Power-ups\*\* (Game-tab -> Effects -> Power-ups, standaard uit): zwevende

- Universeel: \*\*shield\*\* (vangt de volgende dodelijke treffer op, ring om de
- Per game een eigen derde: shooter \*\*rapid fire\*\*, runner \*\*extra sprong\*\*
- Pickups drijven game-afhankelijk binnen (van boven / van rechts) en worden
- 9 nieuwe taalsleutels x 6 talen (150 per taal, pariteit).

 Changed- Shared controller: `loseLife` vangt shield op; render-loop schaalt dt bij
- Geen DB/schema. Over plugin v0.5.0 installeren (`method="upgrade"`).
- Volledig. Power-ups staan standaard uit.

 [ v0.5.0 ](#) Added- "Cooler to play" feel-laag (frontend, geen DB/privacy):

- \*\*Geluidseffecten\*\* (Game-tab -> Effects -> Sound effects, standaard uit):
- \*\*Retro CRT-effect\*\* (Effects -> Retro CRT effect, standaard uit): scanlijnen
- zachte gloed-overlay; flikker uit onder `prefers-reduced-motion`.
- \*\*Juice\*\*: screen-shake op treffers/levensverlies, drijvende score-popups en
- 7 nieuwe taalsleutels x 6 talen (141 per taal, pariteit).

 Changed- Geen wijziging aan bestaande gameplay: de feel-laag haakt uitsluitend op de
- Geen database- of schemawijziging. Installeer over v0.4.x heen
- Volledig backwards compatible. Sound en CRT staan standaard uit; bestaande

 [ v0.4.0 ](#) Added- Reward unlock: beloon de speler die de huidige topscore op een bord verslaat

- Trigger: een echte nieuwe topscore (eerste record, of strikt hoger dan de
- E-mailoptie ("Vraag eerst om e-mail"): de winnaar geeft met toestemming zijn
- Nieuwe per-board tabel `#\_\_yc\_what\_the\_footer\_game\_rewards` met self-healing
- 40 nieuwe taalsleutels in alle zes basistalen (134 per taal, pariteit).

 Changed- `NOTICE.txt` + `README.md`: data-processing-notice herzien - de eerdere "geen
- Additief: tabel `#\_\_yc\_what\_the\_footer\_game\_rewards` wordt aangemaakt bij

 Security- Belonings-inhoud (code, downloadlink, winnaars-inbox) staat uitsluitend
- E-mail-claim is CSRF-getokend en beveiligd met een eenmalig claim-token
- E-mailadres en toestemming worden serverseitig gevalideerd; het adres wordt

 [ v0.3.1 ](#) Fixed- `content.php`: AI-waarschuwingsheader toegevoegd (ontbrak; pre-release-grep-check FAIL).

- `script.php`: `Factory::getDbo()` vervangen door `Factory::getContainer()-&gt;get('DatabaseDriver')`
- `script.php`: hardcoded versiestring in postflight-melding vervangen door
- `template.php`: asset cache-buster gekoppeld aan `WhatTheFooterGame::VERSION` i.p.v.
- `NOTICE.txt`: security-e-mailadres bijgewerkt van `This email address is being protected from spambots. You need JavaScript enabled to view it.` naar

 [ v0.3.0 ](#) Added- Game library: the element now offers FOUR games via the new Game option.

- Each game has its own attract background scene in the footer, its own
- HUD middle slot is game-specific: wave / distance / CPU points / level;

 Changed- game.js refactored into a controller + pluggable game modules (shooter

## Schema

```json
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "Home",
            "item": "https://www.yoozecase.com"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "Extensions",
            "item": "https://www.yoozecase.com/extensions"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "What the F...ooter? ...game!",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
