Interactive Pacman Classroom Game for ESL Students | Teacher Khen Learning Hub (Vocabulary & Reading Game)


Welcome to my interactive classroom game designed for ESL learners. This game helps students practice vocabulary, reading, and picture recognition through a fun Pacman game and Monster Flip activity.

🟡 How to Play the Pacman Game

Step 1: Open Settings ⚙

Step 2: Add your questions:
  • Type words or sentences
  • Paste images using Ctrl + V
Step 3: Set answers:
  • 1 correct answer
  • 3 incorrect answers
  • Click the green circle to mark correct answer
Step 4: Click SAVE & PLAY ▶

Step 5: Play:
  • Drag Pacman to correct answer
  • Correct = +1 point
  • Wrong = -1 life
Game ends when: All questions are finished or lives reach zero.

👾 Monster Flip Game Rule

Flip the cards and answer the question:
  • Read the word 📖
  • Guess the picture 🖼️
  • Choose the correct answer 🧠

🖼️ Image Feature

  • Paste images directly (Ctrl + V)
  • Supports up to 12 slots
  • Works for questions and answers

🧠 Teacher Prompts

Game Prompt:
Create a Pacman-style learning game where students drag Pacman to the correct answer.

Content Prompt:
Make vocabulary or picture-based questions for ESL learners.

Image Prompt:
Allow users to paste images directly into the game.

Goal:
Make learning fun, interactive, and engaging.

💻 FULL HTML GAME CODE


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Teacher Khen Learning Hub</title>
    <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
    <style>
        :root { 
            --bg-main: #93B0AC; 
            --accent: #C5BD96;  
            --header-bg: #C3DCD8; 
            --panel-bg: #FBE8ED;  
            --text-dark: #333;
            --neon-green: #39FF14; 
            --neon-red: #FF3131; 
        }
        body { 
            margin: 0; padding: 0; 
            font-family: 'Andika', 'Comic Sans MS', 'Helvetica Neue', sans-serif; 
            background: var(--bg-main); color: var(--text-dark); 
            overflow: hidden; touch-action: none; 
        }

        .title {
            font-family: 'Press Start 2P', cursive;
            line-height: 1.5;
        }
        
        .screen { display: none; position: absolute; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; z-index: 10; }
        #intro-screen { display: flex; background: url('INTRO.gif') center/cover; }
        
        #game-header {
            position: absolute; top: 0; left: 0; width: 100%; height: 160px;
            display: flex; background: var(--header-bg); border-bottom: 5px solid var(--accent); z-index: 1000;
        }

        .q-box { 
            flex: 1; height: 140px; margin: 10px; background: white; border: 4px solid var(--accent);
            display: flex; align-items: center; padding: 0 15px;
            color: #000; border-radius: 12px; box-sizing: border-box; position: relative;
            justify-content: center;
        }
        
        .q-img-container { width: 120px; height: 110px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
        .q-img-game { max-height: 100%; max-width: 100%; object-fit: contain; }
        
        .q-content { flex: 1; display: flex; flex-direction: column; justify-content: center; }
        .q-text { font-size: 26px; font-weight: bold; line-height: 1.2; padding-right: 40px; text-align: center; }

        .tts-btn {
            position: absolute; right: 10px; bottom: 10px; background: var(--accent); 
            border: 2px solid #fff; border-radius: 50%; width: 35px; height: 35px; 
            cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px;
        }

        #game-boundary { position: absolute; top: 160px; left: 0; width: 100%; height: calc(100% - 160px); display: flex; }
        .game-lane { position: relative; height: 100%; flex: 1; background-size: cover; background-position: center; border-right: 8px solid var(--accent); }

        .lane-info { position: absolute; top: 15px; right: 15px; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; z-index: 800; }
        .team-tag { font-weight: 900; font-size: 18px; color: var(--text-dark); background: rgba(255,255,255,0.7); padding: 2px 10px; border-radius: 10px; border: 2px solid var(--accent); }
        .lane-hp { display: flex; gap: 5px; }
        .hp-heart { width: 35px; height: 35px; object-fit: contain; }

        .shuffle-btn {
            position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: #fff; border: 3px solid var(--accent);
            padding: 10px 25px; border-radius: 25px; font-weight: 900; cursor: pointer; z-index: 2000;
            box-shadow: 0 4px 0 var(--accent); font-size: 14px;
        }
        .shuffle-btn.aligned {
            position: static; transform: none; padding: 6px 20px; border-radius: 20px;
            background: #e67e22; color: #fff; border: 2px solid #000; box-shadow: none;
        }
        .shuffle-btn:active { transform: translateX(-50%) translateY(2px); box-shadow: none; }
        .shuffle-btn.aligned:active { transform: translateY(2px); }

        #bottom-ui {
            position: absolute; bottom: 0; left: 50%; transform: translateX(-50%);
            display: flex; flex-direction: column; align-items: center; z-index: 1100; padding-bottom: 5px;
        }
        .scoreboard {
            background: var(--panel-bg); border: 4px solid var(--accent); border-radius: 10px;
            display: flex; padding: 8px; gap: 10px; box-shadow: 0 0 15px rgba(0,0,0,0.3);
        }
        .score-box {
            background: #fff; color: var(--bg-main); font-family: monospace;
            font-size: 32px; font-weight: bold; padding: 5px 12px; border-radius: 5px;
            border: 2px solid var(--accent); min-width: 50px; text-align: center;
        }
        .ui-btns { display: flex; gap: 10px; margin-top: 5px; align-items: center; }
        .exit-btn, .game-settings-btn {
            background: #3ab0c4; color: #fff; border: 2px solid #000;
            padding: 6px 20px; font-weight: bold; border-radius: 20px; cursor: pointer; font-size: 14px;
        }
        .game-settings-btn { background: var(--accent); }

        .team-end-overlay { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.8); z-index: 900; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
        
        @keyframes vibrate {
            0% { transform: translate(0,0); }
            25% { transform: translate(5px, 5px); }
            50% { transform: translate(-5px, -5px); }
            75% { transform: translate(5px, -5px); }
            100% { transform: translate(0,0); }
        }
        .vibrate { animation: vibrate 0.1s linear infinite; }
        
        .pacman { position: absolute; width: 60px; height: 60px; background: url('PACMAN.gif') center/contain no-repeat; z-index: 500; cursor: grab; touch-action: none; transition: transform 0.1s; }
        .ghost-target { position: absolute; width: 110px; height: 140px; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; pointer-events: none; transition: left 3s linear, top 3s linear; }
        .ghost-img { width: 75px; height: 75px; object-fit: contain; }
        .ans-tag { background: white; color: black; padding: 4px 8px; border-radius: 5px; font-weight: 900; border: 2px solid #000; font-size: 14px; }
        .ans-img-content { width: 55px; height: 55px; object-fit: cover; border: 2px solid white; border-radius: 5px; }

        #settings-modal { display: none; position: fixed; z-index: 3000; inset: 0; background: rgba(0,0,0,0.9); overflow-y: auto; padding: 20px; }
        .modal-content { background: var(--panel-bg); margin: auto; padding: 25px; border-radius: 15px; max-width: 1000px; border: 3px solid var(--accent); color: var(--text-dark); }
        .file-controls { display: flex; justify-content: center; gap: 10px; padding-bottom: 20px; border-bottom: 2px solid var(--accent); margin-bottom: 20px; flex-wrap: wrap; }
        .q-card { background: #fff; padding: 15px; margin-bottom: 15px; border-radius: 10px; border-left: 8px solid var(--bg-main); box-shadow: 2px 2px 5px rgba(0,0,0,0.1); }
        .opt-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
        .btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; text-transform: uppercase; }
    </style>
</head>
<body>

    <audio id="bg-music" loop src="ES_Pixels - Ava Low.mp3"></audio>
    <audio id="sfx-correct" src="correct answer.mp3"></audio>
    <audio id="sfx-wrong" src="wrong.mp3"></audio>

    <div id="intro-screen" class="screen">
        <h1 class="title" style="font-size: 35px; text-shadow: 2px 2px 10px var(--bg-main); color: white; text-align:center;">Teacher Khen<br>Learning Hub</h1>
        <div>
            <button class="btn" style="background:var(--neon-green); color:#000; font-size:30px; margin: 10px;" onclick="saveAndStart()">PLAY ▶</button>
            <button class="btn" style="background:var(--bg-main); color:white; font-size:30px; margin: 10px;" onclick="openSettings()">SETTINGS ⚙</button>
        </div>
    </div>

    <div id="game-screen" class="screen">
        <div id="game-header">
            <div id="box-team-1" class="q-box">
                <div id="q-img-1" class="q-img-container"></div>
                <div id="q-cont-1" class="q-content"><div id="q-text-1" class="q-text"></div></div>
                <button class="tts-btn" onclick="speakQuestion(1)">🔊</button>
            </div>
            <div id="box-team-2" class="q-box">
                <div id="q-img-2" class="q-img-container"></div>
                <div id="q-cont-2" class="q-content"><div id="q-text-2" class="q-text"></div></div>
                <button class="tts-btn" onclick="speakQuestion(2)">🔊</button>
            </div>
        </div>

        <div id="game-boundary">
            <div id="lane-1" class="game-lane">
                <div class="lane-info">
                    <div id="tag-1" class="team-tag">TEAM A</div>
                    <div id="hp-hearts-1" class="lane-hp"></div>
                </div>
                <button id="shuf-1" class="shuffle-btn" onclick="shuffleOptions(1, true)">SHUFFLE 🔀</button>
                <div id="overlay-1" class="team-end-overlay">
                    <img src="GAME OVER.gif" style="max-width: 80%; margin-bottom: 20px;">
                    <button class="btn" style="background:var(--bg-main); color:white;" onclick="restartTeam(1)">TRY AGAIN</button>
                </div>
            </div>
            <div id="lane-2" class="game-lane">
                <div class="lane-info">
                    <div id="tag-2" class="team-tag">TEAM B</div>
                    <div id="hp-hearts-2" class="lane-hp"></div>
                </div>
                <button id="shuf-2" class="shuffle-btn" onclick="shuffleOptions(2, true)">SHUFFLE 🔀</button>
                <div id="overlay-2" class="team-end-overlay">
                    <img src="GAME OVER.gif" style="max-width: 80%; margin-bottom: 20px;">
                    <button class="btn" style="background:var(--bg-main); color:white;" onclick="restartTeam(2)">TRY AGAIN</button>
                </div>
            </div>
        </div>

        <div id="bottom-ui">
            <div class="scoreboard" id="game-scoreboard">
                <div id="score-1" class="score-box">00</div>
                <div id="score-2" class="score-box">00</div>
            </div>
            <div id="btn-group" class="ui-btns">
                <button class="game-settings-btn" onclick="openSettings(true)">SETTINGS ⚙</button>
                <button class="exit-btn" onclick="location.reload()">QUIT</button>
            </div>
        </div>
    </div>

    <div id="settings-modal">
        <div class="modal-content">
            <div class="file-controls">
                <button class="btn" style="background:#2c3e50; color:white;" onclick="exportData()">💾 EXPORT</button>
                <button class="btn" style="background:#2c3e50; color:white;" onclick="document.getElementById('import-file').click()">📁 IMPORT</button>
                <button class="btn" style="background:var(--neon-red); color:white;" onclick="clearAllData()">🗑️ CLEAR DATA</button>
                <input type="file" id="import-file" style="display:none" onchange="importData(this)">
            </div>
            
            <div style="display:flex; gap:15px; margin-bottom:20px; background:rgba(255,255,255,0.5); padding:15px; border-radius:10px; flex-wrap:wrap; align-items:center;">
                Music: <button id="m-btn" class="btn" style="background:#a40000; min-width:60px;" onclick="toggleMusic()">OFF</button>
                Vol: <input type="range" id="vol-ctrl" min="0" max="1" step="0.1" value="0.5" oninput="updateVolume(this.value)">
                Sync: <button id="sync-btn" class="btn" style="background:#4e9a06; min-width:60px;" onclick="toggleSync()">ON</button>
                Lives: <input type="number" id="life-val" value="3" style="width:45px; padding:5px;">
                BG: <select id="bg-sel"><option value="background 1.gif">BG 1</option><option value="background 2.gif">BG 2</option><option value="background 5.gif" selected>BG 5</option></select>
                Mode: <select id="mode-sel"><option value="1">1 Player</option><option value="2" selected>2 Players</option></select>
            </div>
            <div id="q-list"></div>
            <button id="main-save-btn" class="btn" style="width:100%; background:var(--neon-green); color:#000; padding:20px; font-size:24px;" onclick="saveAndStart()">SAVE & PLAY ▶</button>
            <button id="resume-btn" class="btn" style="width:100%; background:var(--bg-main); color:#fff; padding:15px; font-size:20px; margin-top:10px; display:none;" onclick="closeSettings()">RESUME GAME</button>
        </div>
    </div>

    <script>
        const targetGifs = ['target1.gif', 'target2.gif', 'target 3.gif', 'target1.gif'];
        const bgs = ["background 1.gif", "background 2.gif", "background 5.gif"];
        let rawQuestions = Array.from({length: 15}, () => ({
            text: "", img: "", opts: [{t: "", c: true, img: ""}, {t: "", c: false, img: ""}, {t: "", c: false, img: ""}, {t: "", c: false, img: ""}]
        }));
        
        let db, teamData = {}, syncOn = true, isMusicOn = false, gameVolume = 0.5, gameStarted = false;
        let lastFocusedInput = null;
        let moveInterval = null; 
        const music = document.getElementById('bg-music');

        const request = indexedDB.open("TeacherKhenHubDB_vLearning", 1);
        request.onupgradeneeded = e => { db = e.target.result; db.createObjectStore("gameData", { keyPath: "id" }); };
        request.onsuccess = e => { db = e.target.result; loadDB(); };

        function loadDB() {
            const tx = db.transaction("gameData", "readonly");
            const get = tx.objectStore("gameData").get("current");
            get.onsuccess = () => { if(get.result) rawQuestions = get.result.data; };
        }

        function clearAllData() {
            if(confirm("Are you sure you want to clear all questions and images? This cannot be undone.")) {
                rawQuestions = Array.from({length: 15}, () => ({
                    text: "", img: "", opts: [{t: "", c: true, img: ""}, {t: "", c: false, img: ""}, {t: "", c: false, img: ""}, {t: "", c: false, img: ""}]
                }));
                if(db) {
                    const tx = db.transaction("gameData", "readwrite");
                    tx.objectStore("gameData").clear();
                }
                openSettings(gameStarted);
            }
        }

        window.addEventListener('paste', async (e) => {
            if (!lastFocusedInput) return;
            const items = e.clipboardData.items;
            for (let item of items) {
                if (item.type.indexOf("image") !== -1) {
                    const blob = item.getAsFile();
                    const reader = new FileReader();
                    reader.onload = (event) => {
                        const type = lastFocusedInput.dataset.type;
                        const qi = parseInt(lastFocusedInput.dataset.qi);
                        const oi = lastFocusedInput.dataset.oi ? parseInt(lastFocusedInput.dataset.oi) : null;
                        if(type === 'q') rawQuestions[qi].img = event.target.result;
                        else rawQuestions[qi].opts[oi].img = event.target.result;
                        openSettings(gameStarted);
                    };
                    reader.readAsDataURL(blob);
                }
            }
        });

        function toggleMusic() { 
            isMusicOn = !isMusicOn; isMusicOn ? music.play() : music.pause();
            document.getElementById('m-btn').innerText = isMusicOn ? "ON" : "OFF";
            document.getElementById('m-btn').style.background = isMusicOn ? "#4e9a06" : "#a40000";
        }

        function updateVolume(val) {
            gameVolume = val; music.volume = val;
            document.getElementById('sfx-correct').volume = val;
            document.getElementById('sfx-wrong').volume = val;
        }

        function toggleSync() {
            syncOn = !syncOn; document.getElementById('sync-btn').innerText = syncOn ? "ON" : "OFF";
            document.getElementById('sync-btn').style.background = syncOn ? "#4e9a06" : "#666";
        }

        function speakQuestion(id) {
            const text = teamData[id].questions[teamData[id].currentIdx].text;
            if(!text) return;
            window.speechSynthesis.cancel();
            const msg = new SpeechSynthesisUtterance(text);
            msg.rate = 0.8;
            window.speechSynthesis.speak(msg);
        }

        function shuffleOptions(id, isQuick = false) {
            const lane = document.getElementById(`lane-${id}`);
            const targets = lane.querySelectorAll('.ghost-target');
            const quadrants = [
                {x: 5, y: 5},   {x: 75, y: 5},
                {x: 5, y: 65},  {x: 75, y: 65}
            ].sort(() => Math.random() - 0.5);

            targets.forEach((g, i) => {
                if (isQuick) {
                    g.style.transition = "left 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), top 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
                    setTimeout(() => { if(g) g.style.transition = "left 3s linear, top 3s linear"; }, 500);
                } else {
                    g.style.transition = "left 3s linear, top 3s linear";
                }
                const pos = quadrants[i % quadrants.length];
                const driftX = Math.random() * 10;
                const driftY = Math.random() * 10;
                g.style.left = (pos.x + driftX) + "%";
                g.style.top = (pos.y + driftY) + "%";
            });
        }

        function startMovement() {
            if(moveInterval) clearInterval(moveInterval);
            moveInterval = setInterval(() => {
                if(document.getElementById('settings-modal').style.display === 'block') return;
                [1, 2].forEach(id => {
                    if(teamData[id] && teamData[id].active) shuffleOptions(id);
                });
            }, 3000);
        }

        function openSettings(duringGame = false) {
            const list = document.getElementById('q-list'); list.innerHTML = '';
            const saveBtn = document.getElementById('main-save-btn');
            const resumeBtn = document.getElementById('resume-btn');
            saveBtn.style.display = duringGame ? "none" : "block";
            resumeBtn.style.display = duringGame ? "block" : "none";

            rawQuestions.forEach((q, i) => {
                const card = document.createElement('div'); card.className = 'q-card';
                card.innerHTML = `
                    <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
                        ${q.img ? `<img src="${q.img}" style="width:50px; height:50px; object-fit:cover;">` : ''}
                        <input type="text" value="${q.text}" data-type="q" data-qi="${i}" 
                            onfocus="lastFocusedInput=this" oninput="rawQuestions[${i}].text=this.value" 
                            style="flex:1; padding:12px; font-family: sans-serif;" placeholder="Question ${i+1}">
                    </div>
                    <div class="opt-grid">${q.opts.map((o, oi) => `
                        <div style="display:flex; background:#eee; padding:5px; border-radius:5px;">
                            ${o.img ? `<img src="${o.img}" style="width:30px; height:30px;">` : ''}
                            <input type="text" value="${o.t}" data-type="o" data-qi="${i}" data-oi="${oi}"
                                onfocus="lastFocusedInput=this" oninput="rawQuestions[${i}].opts[${oi}].t=this.value" 
                                style="flex:1; font-family: sans-serif;">
                            <div style="cursor:pointer; background:${o.c? 'var(--neon-green)':'#ccc'}; width:25px; height:25px; border-radius:50%;" onclick="setCorrect(${i},${oi})"></div>
                        </div>`).join('')}</div>`;
                list.appendChild(card);
            });
            document.getElementById('settings-modal').style.display = 'block';
        }

        function closeSettings() { document.getElementById('settings-modal').style.display = 'none'; }
        function setCorrect(qi, oi) { rawQuestions[qi].opts.forEach((o, i) => o.c = (i === oi)); openSettings(gameStarted); }

        function saveAndStart() {
            gameStarted = true;
            if(db) db.transaction("gameData", "readwrite").objectStore("gameData").put({ id: "current", data: rawQuestions });
            const valid = rawQuestions.filter(q => q.text.trim() !== "" || q.img !== "");
            const selBg = document.getElementById('bg-sel').value;
            const mode = parseInt(document.getElementById('mode-sel').value);

            [1, 2].forEach(id => {
                let qs = JSON.parse(JSON.stringify(valid));
                if(!syncOn) qs.sort(() => Math.random() - 0.5);
                teamData[id] = { questions: qs, currentIdx: 0, lives: parseInt(document.getElementById('life-val').value), score: 0, active: true, isStunned: false };
                document.getElementById(`lane-${id}`).style.backgroundImage = `url('${id === 1 ? selBg : (bgs.find(b => b !== selBg) || bgs[1])}')`;
                updateHearts(id);
            });

            const shuf1 = document.getElementById('shuf-1');
            const btnGroup = document.getElementById('btn-group');
            if(mode === 1) {
                shuf1.classList.add('aligned');
                btnGroup.prepend(shuf1); 
            } else {
                shuf1.classList.remove('aligned');
                document.getElementById('lane-1').appendChild(shuf1); 
            }

            document.getElementById('box-team-2').style.display = mode === 1 ? "none" : "flex";
            document.getElementById('lane-2').style.display = mode === 1 ? "none" : "block";
            document.getElementById('game-scoreboard').style.display = mode === 1 ? "none" : "flex";
            document.getElementById('tag-1').style.display = mode === 1 ? "none" : "block";

            closeSettings();
            document.getElementById('intro-screen').style.display = 'none';
            document.getElementById('game-screen').style.display = 'flex';
            spawnPlayer(1); if(mode === 2) spawnPlayer(2);
            loadQ(1); if(mode === 2) loadQ(2);
            startMovement();
        }

        function updateHearts(id) {
            const container = document.getElementById(`hp-hearts-${id}`);
            container.innerHTML = '';
            for(let i=0; i < teamData[id].lives; i++) container.innerHTML += `<img src="life.gif" class="hp-heart">`;
        }

        function spawnPlayer(id) {
            const lane = document.getElementById(`lane-${id}`);
            lane.querySelectorAll('.pacman').forEach(p => p.remove());
            const p = document.createElement('div'); p.className = 'pacman'; p.id = `pac-${id}`;
            p.style.left = '45%'; p.style.top = '70%';
            if(id === 2) p.style.filter = 'hue-rotate(160deg)';
            lane.appendChild(p);
            drag(p, id);
        }

        function loadQ(id) {
            const data = teamData[id];
            const lane = document.getElementById(`lane-${id}`);
            lane.querySelectorAll('.ghost-target').forEach(g => g.remove());
            if(data.currentIdx >= data.questions.length) { endTeam(id); return; }

            const q = data.questions[data.currentIdx];
            const qBox = document.getElementById(`box-team-${id}`);
            const qImgCont = document.getElementById(`q-img-${id}`);
            const qContent = document.getElementById(`q-cont-${id}`);
            const qText = document.getElementById(`q-text-${id}`);

            qText.innerText = q.text;
            qImgCont.innerHTML = q.img ? `<img src="${q.img}" class="q-img-game">` : '';

            if(q.text && q.img) {
                qBox.style.flexDirection = "row";
                qImgCont.style.display = "flex";
                qImgCont.style.width = "120px";
                qText.style.textAlign = "left";
            } else if(q.img) {
                qBox.style.flexDirection = "row";
                qImgCont.style.display = "flex";
                qImgCont.style.width = "100%"; 
                qContent.style.display = "none";
            } else {
                qBox.style.flexDirection = "row";
                qImgCont.style.display = "none";
                qContent.style.display = "flex";
                qText.style.textAlign = "center";
            }

            q.opts.forEach((o, i) => {
                const g = document.createElement('div'); g.className = 'ghost-target';
                const img = o.img ? `<img src="${o.img}" class="ans-img-content">` : '';
                g.innerHTML = `${img}<img src="${targetGifs[i]}" class="ghost-img"><span class="ans-tag">${o.t}</span>`;
                g.dataset.c = o.c; lane.appendChild(g);
            });
            shuffleOptions(id);
        }

        function drag(el, id) {
            const moveHandler = (e) => {
                if(!teamData[id].active || teamData[id].isStunned || document.getElementById('settings-modal').style.display === 'block') return;
                const evt = e.touches ? Array.from(e.touches).find(t => t.target === el || el.contains(t.target)) || e.touches[0] : e;
                const r = el.parentElement.getBoundingClientRect();
                el.style.left = (evt.clientX - r.left - 30) + 'px';
                el.style.top = (evt.clientY - r.top - 30) + 'px';
                check(el, id);
                e.preventDefault();
            };
            const stopHandler = () => {
                el.removeEventListener('mousemove', moveHandler);
                el.removeEventListener('touchmove', moveHandler);
            };
            el.addEventListener('mousedown', () => el.addEventListener('mousemove', moveHandler));
            el.addEventListener('touchstart', (e) => el.addEventListener('touchmove', moveHandler, { passive: false }), { passive: false });
            window.addEventListener('mouseup', stopHandler);
            window.addEventListener('touchend', stopHandler);
        }

        function check(p, id) {
            const pR = p.getBoundingClientRect();
            p.parentElement.querySelectorAll('.ghost-target').forEach(g => {
                const gR = g.getBoundingClientRect();
                if(!(pR.right < gR.left || pR.left > gR.right || pR.bottom < gR.top || pR.top > gR.bottom)) {
                    if(g.dataset.c === "true") {
                        document.getElementById('sfx-correct').play();
                        teamData[id].score++;
                        const sc = document.getElementById(`score-${id}`);
                        if(sc) sc.innerText = teamData[id].score.toString().padStart(2, '0');
                        teamData[id].currentIdx++;
                        loadQ(id);
                    } else {
                        document.getElementById('sfx-wrong').play();
                        teamData[id].lives--;
                        updateHearts(id);
                        g.remove();
                        teamData[id].isStunned = true;
                        p.classList.add('vibrate');
                        setTimeout(() => {
                            teamData[id].isStunned = false;
                            p.classList.remove('vibrate');
                        }, 2000);
                        if(teamData[id].lives <= 0) endTeam(id);
                    }
                }
            });
        }

        function endTeam(id) { teamData[id].active = false; document.getElementById(`overlay-${id}`).style.display = 'flex'; }
        
        function restartTeam(id) {
            const valid = rawQuestions.filter(q => q.text.trim() !== "" || q.img !== "");
            let qs = JSON.parse(JSON.stringify(valid));
            if(!syncOn) qs.sort(() => Math.random() - 0.5);
            teamData[id] = { questions: qs, currentIdx: 0, lives: parseInt(document.getElementById('life-val').value), score: 0, active: true, isStunned: false };
            const sc = document.getElementById(`score-${id}`);
            if(sc) sc.innerText = "00";
            updateHearts(id);
            document.getElementById(`overlay-${id}`).style.display = 'none';
            spawnPlayer(id); loadQ(id);
        }

        function exportData() {
            const blob = new Blob([JSON.stringify(rawQuestions)], {type: 'application/json'});
            const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'TeacherKhenLearningHub.json'; a.click();
        }
        function importData(i) {
            const r = new FileReader(); r.onload = (e) => { rawQuestions = JSON.parse(e.target.result); openSettings(); }; r.readAsText(i.files[0]);
        }
    </script>
</body>
</html>


Post a Comment

7 Comments

  1. Thank you for sharing, Teacher Khen! God bless..

    ReplyDelete
  2. Hi teacher khen! I tried customizing it at nagwork siya sa iisang PC pero nung triny ko isalpak yung flash drive sa ibang PC wala na yung mga characters na pinaste ko🥹

    ReplyDelete
    Replies
    1. Teach, export or save file muna tpos open site sa ibng computer then i-load file mo dun sa website

      Delete
    2. Teach saang website? Kasi dinowload ko yung buo from your github and nagkaroon ako ng file na “Pacman Main” then copy ko ang index file sa USB ko after ko nilagyan ng vocabs and pictures pero nung sinalpak ko ang USB ko sa ibang PC, wala siya laman🥹

      Delete
    3. Hi teach, yung sa mismong link teach.
      1. Open the github.
      2. Go to setting.
      3. List your own words and add pictures.
      4. Export file/Save file
      5. Copy the saved file (.json file)
      6. Open github to another computer
      7. Go to setting
      8. Load file (choose the .json file you saved)

      Delete
  3. Will try this today teach, thank you so much teach! Have been struggling thinking ano ipapalaro sa bata at first timeko din magturo ng elem.dito sa Taiwan, and same ata tayo ng grade level na tinuturuan kasi same po sa mga games na nagagawa niyo ang content ng book namin.hehe thank you po talaga!

    ReplyDelete