(function(){
'use strict';
function shuffle(arr){
const a=arr.slice();
for(let i=a.length - 1; i > 0; i--){
const j=Math.floor(Math.random() * (i + 1));
[a[i], a[j]]=[a[j], a[i]];
}
return a;
}
function createMusicController(){
let audioCtx=null;
let gainNode=null;
let timer=null;
let enabled=true;
const notes=[220, 261.63, 293.66, 329.63, 392.0, 329.63, 293.66, 261.63];
let noteIndex=0;
function ensureCtx(){
if(audioCtx) return;
const Ctx=window.AudioContext||window.webkitAudioContext;
if(!Ctx) return;
audioCtx=new Ctx();
gainNode=audioCtx.createGain();
gainNode.gain.value=0.025;
gainNode.connect(audioCtx.destination);
}
function beep(freq, duration){
if(!enabled) return;
ensureCtx();
if(!audioCtx||!gainNode) return;
const osc=audioCtx.createOscillator();
osc.type='sine';
osc.frequency.value=freq;
osc.connect(gainNode);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function start(){
if(timer) return;
ensureCtx();
if(audioCtx&&audioCtx.state==='suspended'){
audioCtx.resume().catch(function(){});
}
timer=window.setInterval(function(){
if(enabled){
beep(notes[noteIndex % notes.length], 0.18);
noteIndex++;
}}, 520);
}
function stop(){
if(timer){
window.clearInterval(timer);
timer=null;
}}
function toggle(){
enabled = !enabled;
if(enabled){ start(); }
return enabled;
}
return { start, stop, toggle, isEnabled: function(){ return enabled; }};}
function initRunner(app){
const shell=app.querySelector('.emh-runner-shell');
const stage=app.querySelector('.emh-runner-stage');
const road=app.querySelector('.emh-road');
const itemsLayer=app.querySelector('.emh-items-layer');
const player=app.querySelector('.emh-player');
const message=app.querySelector('.emh-runner-hud-message');
const scoreEl=app.querySelector('.emh-score');
const bestEl=app.querySelector('.emh-best');
const livesEl=app.querySelector('.emh-lives');
const wordEl=app.querySelector('.emh-target-word');
const scoreMirrorEl=app.querySelector('.emh-score-mirror');
const wordMirrorEl=app.querySelector('.emh-target-word-mirror');
const startBtn=app.querySelector('.emh-start-btn');
const fsBtn=app.querySelector('.emh-fullscreen-btn');
const musicBtn=app.querySelector('.emh-music-btn');
const leftBtn=app.querySelector('.emh-move-left');
const rightBtn=app.querySelector('.emh-move-right');
const overlayBtn=app.querySelector('.emh-overlay-btn');
const lanePositionsNear=[18, 50, 82];
const lanePositionsFar=[42, 50, 58];
let laneIndex=1;
let words=[];
let score=0;
let best=parseInt(localStorage.getItem('emhRunnerBestScore')||'0', 10)||0;
let active=false;
let currentTarget=null;
let currentItems=[];
let wrongHits=0;
let gameLoop=null;
let spawnTimeout=null;
let stageHeight=0;
const baseSpeed=0.3;
let speed=baseSpeed;
let touchX=null;
const music=createMusicController();
bestEl.textContent=String(best);
if(scoreMirrorEl){ scoreMirrorEl.textContent=String(score); }
if(wordMirrorEl){ wordMirrorEl.textContent='-'; }
function updateMessage(text){
message.textContent=text||'';
}
function updateMusicButton(){
musicBtn.textContent=music.isEnabled() ? EMHRunnerData.strings.musicOn:EMHRunnerData.strings.musicOff;
}
function setLane(index){
laneIndex=Math.max(0, Math.min(2, index));
player.style.left=lanePositionsNear[laneIndex] + '%';
}
function moveLeft(){ setLane(laneIndex - 1); }
function moveRight(){ setLane(laneIndex + 1); }
function updateLives(){
const remaining=Math.max(0, 3 - wrongHits);
livesEl.textContent=String(remaining);
}
function setScore(next){
score=next;
scoreEl.textContent=String(score);
if(scoreMirrorEl){ scoreMirrorEl.textContent=String(score); }
if(score > best){
best=score;
bestEl.textContent=String(best);
localStorage.setItem('emhRunnerBestScore', String(best));
}
speed=baseSpeed * (1 + (Math.floor(score / 25) * 0.05));
}
function clearItems(){
currentItems.forEach(function(item){ if(item.el&&item.el.parentNode){ item.el.parentNode.removeChild(item.el); }});
currentItems=[];
itemsLayer.innerHTML='';
}
function randChoice(arr){ return arr[Math.floor(Math.random() * arr.length)]; }
function renderIcon(item){
if(item.iconType==='image'&&item.imageUrl){
return '<img src="' + item.imageUrl.replace(/"/g, '&quot;') + '" alt="">';
}
return '<span>' + (item.emoji||'⭐') + '</span>';
}
function chooseRound(){
const all=shuffle(words);
currentTarget=all[0];
const distractorCount=Math.random() > 0.45 ? 2:1;
const distractors=all.filter(function(w){ return w.id!==currentTarget.id; }).slice(0, distractorCount);
const options=shuffle([currentTarget].concat(distractors));
wordEl.textContent=currentTarget.word;
if(wordMirrorEl){ wordMirrorEl.textContent=currentTarget.word; }
clearItems();
const lanes=shuffle([0,1,2]);
options.forEach(function(item, index){
const el=document.createElement('div');
el.className='emh-falling-item';
const lane=lanes[index];
el.style.left=lanePositionsFar[lane] + '%';
el.style.top='-10%';
el.innerHTML=renderIcon(item);
itemsLayer.appendChild(el);
currentItems.push({ el: el, lane: lane, y: -10, item: item, hit: false });
});
}
function loseLife(reasonText){
wrongHits +=1;
updateLives();
updateMessage(reasonText||'❌ Oops!');
clearItems();
if(wrongHits >=3){
gameOver();
return;
}
nextRoundSoon();
}
function gameOver(){
active=false;
if(gameLoop){ cancelAnimationFrame(gameLoop); gameLoop=null; }
if(spawnTimeout){ clearTimeout(spawnTimeout); spawnTimeout=null; }
overlayBtn.hidden=false;
overlayBtn.textContent=EMHRunnerData.strings.gameOver + ' • ' + EMHRunnerData.strings.playAgain;
overlayBtn.classList.add('is-visible');
updateMessage('');
music.stop();
}
function nextRoundSoon(){
if(spawnTimeout) clearTimeout(spawnTimeout);
spawnTimeout=setTimeout(function(){
if(active){ chooseRound(); }}, 380);
}
function tick(){
if(!active) return;
stageHeight=road.clientHeight||500;
let anyVisible=false;
currentItems.forEach(function(obj){
if(obj.hit) return;
anyVisible=true;
obj.y +=speed;
const progress=Math.max(0, Math.min(1, (obj.y + 10) / 110));
const scale=0.28 + (progress * 1.06);
const opacity=0.55 + (progress * 0.45);
const laneLeft=lanePositionsFar[obj.lane] + ((lanePositionsNear[obj.lane] - lanePositionsFar[obj.lane]) * progress);
obj.el.style.left=laneLeft.toFixed(3) + '%';
obj.el.style.top=obj.y + '%';
obj.el.style.transform='translateX(-50%) scale(' + scale.toFixed(3) + ')';
obj.el.style.opacity=opacity.toFixed(3);
if(obj.y >=72&&obj.y <=92&&obj.lane===laneIndex){
obj.hit=true;
obj.el.classList.add(obj.item.id===currentTarget.id ? 'is-good':'is-bad');
if(obj.item.id===currentTarget.id){
setScore(score + 1);
updateMessage('⭐ Great!');
clearItems();
nextRoundSoon();
}else{
loseLife('❌ Wrong answer!');
}}else if(obj.y > 110&&obj.item.id===currentTarget.id&&!obj.hit){
loseLife('❌ Missed it!');
}});
if(active){ gameLoop=requestAnimationFrame(tick); }}
function fetchWords(){
updateMessage(EMHRunnerData.strings.loading);
const body=new URLSearchParams();
body.set('action', 'emh_runner_get_words');
body.set('nonce', EMHRunnerData.nonce);
return fetch(EMHRunnerData.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
credentials: 'same-origin'
}).then(function(r){ return r.json(); }).then(function(json){
if(!json||!json.success||!json.data||!Array.isArray(json.data.items)){
throw new Error('Invalid response');
}
words=json.data.items;
if(words.length < 3){
updateMessage(EMHRunnerData.strings.notEnough);
return false;
}
return true;
}).catch(function(){
updateMessage('Could not load game items.');
return false;
});
}
function startGame(){
overlayBtn.hidden=true;
overlayBtn.classList.remove('is-visible');
clearItems();
setLane(1);
setScore(0);
wrongHits=0;
updateLives();
active=false;
fetchWords().then(function(ok){
if(!ok) return;
updateMessage('');
active=true;
chooseRound();
if(gameLoop){ cancelAnimationFrame(gameLoop); }
gameLoop=requestAnimationFrame(tick);
music.start();
updateMusicButton();
stage.focus();
});
}
function isNativeFullscreenActive(){
return !!(document.fullscreenElement||document.webkitFullscreenElement||document.msFullscreenElement);
}
function isIOSLike(){
const ua=window.navigator.userAgent||'';
const touchMac=navigator.platform==='MacIntel'&&navigator.maxTouchPoints > 1;
return /iPhone|iPad|iPod/i.test(ua)||touchMac;
}
function isMobileFallbackFullscreen(){
return app.classList.contains('is-mobile-fullscreen');
}
function enterMobileFullscreen(){
app.classList.add('is-mobile-fullscreen');
document.body.classList.add('emh-runner-no-scroll');
setTimeout(function(){ window.scrollTo(0, 0); }, 20);
refreshFsButton();
}
function exitMobileFullscreen(){
app.classList.remove('is-mobile-fullscreen');
document.body.classList.remove('emh-runner-no-scroll');
refreshFsButton();
}
function toggleFullscreen(){
const target=shell;
const req=target.requestFullscreen||target.webkitRequestFullscreen||target.msRequestFullscreen;
const exit=document.exitFullscreen||document.webkitExitFullscreen||document.msExitFullscreen;
if(isNativeFullscreenActive()){
if(exit){ exit.call(document); }
return;
}
if(isMobileFallbackFullscreen()){
exitMobileFullscreen();
return;
}
if(req&&!isIOSLike()){
Promise.resolve(req.call(target)).catch(function(){
enterMobileFullscreen();
});
}else{
enterMobileFullscreen();
}}
function refreshFsButton(){
fsBtn.textContent=(isNativeFullscreenActive()||isMobileFallbackFullscreen()) ? EMHRunnerData.strings.exitFullscreen:EMHRunnerData.strings.fullscreen;
}
startBtn.addEventListener('click', startGame);
overlayBtn.addEventListener('click', startGame);
fsBtn.addEventListener('click', toggleFullscreen);
musicBtn.addEventListener('click', function(){
music.toggle();
updateMusicButton();
});
leftBtn.addEventListener('click', function(){ stage.focus(); moveLeft(); });
rightBtn.addEventListener('click', function(){ stage.focus(); moveRight(); });
document.addEventListener('fullscreenchange', refreshFsButton);
document.addEventListener('webkitfullscreenchange', refreshFsButton);
window.addEventListener('resize', refreshFsButton);
updateMusicButton();
refreshFsButton();
stage.addEventListener('keydown', function(e){
if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A'){
e.preventDefault(); moveLeft();
}else if(e.key==='ArrowRight'||e.key==='d'||e.key==='D'){
e.preventDefault(); moveRight();
}});
stage.addEventListener('touchstart', function(e){
if(e.touches[0]) touchX=e.touches[0].clientX;
}, {passive:true});
stage.addEventListener('touchend', function(e){
if(touchX===null) return;
const endX=(e.changedTouches[0]&&e.changedTouches[0].clientX)||touchX;
const diff=endX - touchX;
if(Math.abs(diff) > 24){
if(diff < 0) moveLeft(); else moveRight();
}else{
const rect=stage.getBoundingClientRect();
if(endX < rect.left + (rect.width / 2)) moveLeft(); else moveRight();
}
touchX=null;
}, {passive:true});
setLane(1);
updateLives();
updateMessage('Press Start Game.');
}
document.addEventListener('DOMContentLoaded', function(){
document.querySelectorAll('.emh-runner-app').forEach(initRunner);
});
})();
(function(){
document.addEventListener('keydown', function(e){
if(e.key==='Escape'){
document.querySelectorAll('.emh-runner-app.is-mobile-fullscreen').forEach(function(appEl){
appEl.classList.remove('is-mobile-fullscreen');
});
document.body.classList.remove('emh-runner-no-scroll');
document.querySelectorAll('.emh-fullscreen-btn').forEach(function(btn){
btn.textContent=(window.EMHRunnerData&&EMHRunnerData.strings&&EMHRunnerData.strings.fullscreen) ? EMHRunnerData.strings.fullscreen:'Full Screen';
});
}});
})();