Free Interactive ESL Spelling Game Using HTML, CSS, and JavaScript for Teachers



Traditional spelling drills can become repetitive for young ESL learners. To make spelling practice more engaging and interactive, I created a customizable HTML spelling game using HTML, CSS, and JavaScript.

This project is designed for teachers who want a fun classroom activity that combines movement, vocabulary recall, sound effects, and gamification.

The game can be customized easily by changing vocabulary words, images, sounds, and gameplay settings.


PART 1 — How to Play the Game

🎮 Main Gameplay

Students control a character and move up or down to collect the correct floating letters across the screen.

Each correct letter fills one spelling box. Wrong letters reduce hearts or HP. Once the whole word is completed correctly, the player earns a point and proceeds to the next vocabulary word.

✨ Main Features

  • Solo Mode
  • Team VS Mode
  • Character Selection Screen
  • Floating Letter Mechanics
  • Background Music and Sound Effects
  • Editable Vocabulary Settings
  • Image-Based Vocabulary Questions
  • Save and Load JSON Lesson Files
  • Mobile-Friendly Controls
  • Adjustable Speed and Timer

📚 Educational Benefits

  • Improves spelling recognition
  • Encourages active vocabulary recall
  • Increases classroom engagement
  • Supports visual learners through images
  • Promotes healthy classroom competition

📝 How to Play

  1. Open the spelling game.
  2. Go to the Settings menu.
  3. Add vocabulary words and hints.
  4. Select Solo or Team Mode.
  5. Choose characters.
  6. Collect the correct letters in order.
  7. Complete the word before time runs out.

💡 Classroom Tips

You can organize vocabulary by category such as:

  • Animals
  • Food
  • School Supplies
  • Colors
  • Transportation
  • Phonics Patterns

For beginners, use shorter words and slower speed settings. For advanced learners, increase the game speed and use longer vocabulary words.

PART 2 — AI Prompt Used to Generate the Game

Below is the detailed AI prompt used to generate the same style, mechanics, and overall design of the spelling game.

The prompt includes:

  • Gameplay mechanics
  • Visual design requirements
  • Educational features
  • Teacher customization tools
  • Sound and animation effects

🤖 Complete AI Prompt

Create a full-screen interactive ESL spelling game using HTML, CSS, and JavaScript. STYLE REQUIREMENTS: - Use elegant soft color combinations: background: #F0F7EE dark accent: #776871 secondary accent: #91A8A4 soft blue accent: rgba(196, 215, 242, 0.8) - Add smooth modern UI design - Add rounded buttons and soft shadows - Use Comic Sans font for floating letters - Add blurred overlay effects - Use animated shiny hover effects on buttons - Add stylish title screen - Make the game visually appealing for elementary ESL learners GAMEPLAY REQUIREMENTS: - Full screen responsive gameplay - Mobile-friendly controls - Character movement using UP and DOWN buttons - Floating letters move horizontally across the screen - Students collect letters in correct spelling order - Wrong letters reduce HP hearts - Correct letters fill spelling boxes - Completed words increase score - Add timer system - Add score system - Add game over screen - Add pause effect after completing a word - Add NEXT button before next word GAME MODES: - Solo Mode - Team VS Mode CHARACTER FEATURES: - Character selection screen - Multiple selectable character images - Animated character sprites QUESTION SYSTEM: - Editable vocabulary database - Hint text support - Image-based question support - Paste image directly from clipboard - Randomized question order - Randomized letter spawning SETTINGS MENU: - Timer adjustment - Speed adjustment - HP adjustment - Music ON/OFF - SFX ON/OFF - Volume control - Save settings automatically using localStorage FILE FEATURES: - Save lesson file as JSON - Load lesson JSON file - Auto-save settings AUDIO: - Background music - Correct answer sound - Wrong answer sound - Victory sound - Button click sound VISUAL EFFECTS: - Glow effect when word is completed - Soft shadows - Overlay tint background - Floating letter animations TECHNICAL REQUIREMENTS: - Use only HTML, CSS, and vanilla JavaScript - No external libraries - Everything must work offline - Organize code clearly - Make code beginner-friendly and editable for teachers

📁 Required Files

  • background image.webp
  • background music.mp3
  • correct answer.mp3
  • wrong.mp3
  • next word.mp3
  • ting.mp3
  • chime.mp3
  • pikachu.png
  • mario.gif
  • Luigi.gif
  • Sir Axiom.gif
  • character3.gif

💻 Complete HTML Game Code

Paste your complete spelling game HTML code below:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Teacher Khen Learning Hub [Spelling Game]</title>
    <style>
        :root { --bg: #F0F7EE; --dark: #776871; --accent: rgba(196, 215, 242, 0.8); --sec: #91A8A4; --win: #27ae60; }
        * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
        
        body, html { margin: 0; padding: 0; width: 100%; height: 100%; font-family: Arial, Helvetica, sans-serif; overflow: hidden; }
        
        .main-bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: url('background image.webp') no-repeat center center; background-size: cover; z-index: -2; }
        .overlay-tint { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.2); z-index: -1; }

        .menu-header { text-align: center; margin-bottom: 30px; z-index: 5; }
        .main-title-gothic { font-size: 80px; color: white; text-shadow: 4px 4px 10px black; margin: 0; font-weight: normal; }
        .sub-title-clean { font-size: 24px; color: #FFD700; text-shadow: 2px 2px 4px black; margin-top: -10px; font-weight: bold; }

        .screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 10; }
        .active { display: flex !important; }
        
        .menu-btn { background: var(--dark); border: 3px solid white; padding: 12px 25px; font-size: 18px; color: white; border-radius: 50px; cursor: pointer; margin: 5px; font-weight: bold; position: relative; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.3); }
        .shine { position: absolute; top: 0; left: -100%; width: 50%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); transition: 0.5s; }
        .menu-btn:hover .shine { left: 150%; }

        #central-scoreboard { position: fixed; top: 35%; left: 50%; transform: translate(-50%, -50%); display: none; background: rgba(255,255,255,0.9); padding: 5px 25px; border-radius: 50px; border: 4px solid var(--dark); z-index: 1000; backdrop-filter: blur(5px); box-shadow: 0 0 20px rgba(0,0,0,0.5); }
        .score-num { font-weight: 900; font-size: 40px; color: var(--dark); margin: 0 15px; }
        
        #gameplay-screen { flex-direction: row; display: none; background: transparent; }
        .game-half { position: relative; flex: 1; height: 100%; border-right: 2px solid rgba(255,255,255,0.3); display: flex; flex-direction: column; overflow: hidden; }

        .hud { background: rgba(0,0,0,0.5); color: white; padding: 10px 20px; display: flex; align-items: center; min-height: 50px; }
        .hud-t1 { justify-content: space-between; }
        .hud-t2 { justify-content: flex-end; gap: 20px; } 

        .single-score-ui { font-size: 24px; font-weight: bold; background: var(--win); padding: 2px 12px; border-radius: 10px; border: 2px solid white; }

        .question-area { height: 35%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.4); padding: 10px; border-bottom: 3px solid white; backdrop-filter: blur(3px); position: relative; }
        .question-text { font-size: 30px; font-weight: bold; color: white; text-align: center; text-shadow: 2px 2px 4px black; }
        .question-img { max-height: 100px; border: 3px solid white; border-radius: 8px; margin-bottom: 5px; }

        .boxes { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
        .box { width: 42px; height: 42px; background: rgba(255,255,255,0.9); border: 2px solid var(--dark); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 26px; border-radius: 8px; color: var(--dark); }
        
        .completion-layer { position: absolute; right: 20px; top: 50%; transform: translateY(-50%); display: none; z-index: 200; }
        .final-word { font-size: 48px; color: #FFD700; font-weight: 900; text-shadow: 2px 2px 10px rgba(0,0,0,0.8); }

        .lane { height: 65%; position: relative; width: 100%; background: transparent; overflow: hidden; }
        .fixed-controls { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 15px; z-index: 50; }
        .arrow-btn { width: 80px; height: 80px; background: rgba(255,255,255,0.2); border: 3px solid white; border-radius: 15px; font-size: 40px; cursor: pointer; color: white; display: flex; align-items: center; justify-content: center; }
        
        .char-container { position: absolute; left: 100px; z-index: 100; pointer-events: none; }
        .paused { filter: brightness(1.3) drop-shadow(0 0 20px gold); }
        .char { width: 110px; height: 110px; object-fit: contain; }
        
        /* Comic Sans Font for Letters */
        .letter { position: absolute; width: 65px; height: 65px; background: radial-gradient(circle, #fff, var(--sec)); border: 3px solid var(--dark); color: var(--dark); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 38px; font-weight: bold; z-index: 60; box-shadow: 0 4px 8px rgba(0,0,0,0.3); font-family: "Comic Sans MS", "Comic Sans", cursive; }

        .settings-content { background: white; width: 95%; height: 95%; padding: 25px; border-radius: 20px; border: 8px solid var(--dark); overflow-y: auto; }
        .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
        .s-box { background: #eef2f7; padding: 12px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; font-weight: bold; color: var(--dark); border: 1px solid #ccc; }
        .editor-table { width: 100%; border-collapse: collapse; background: white; margin-top: 20px; }
        .editor-table th, .editor-table td { border: 1px solid #ddd; padding: 10px; text-align: center; }
        
        .game-over-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); color: white; z-index: 500; display: none; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
    </style>
</head>
<body onpaste="handlePaste(event)">

<div class="main-bg"></div>
<div class="overlay-tint"></div>

<audio id="bgMusic" loop><source src="background music.mp3" type="audio/mpeg"></audio>
<audio id="sfxCorrect"><source src="correct answer.mp3" type="audio/mpeg"></audio>
<audio id="sfxWrong"><source src="wrong.mp3" type="audio/mpeg"></audio>
<audio id="sfxNext"><source src="next word.mp3" type="audio/mpeg"></audio>
<audio id="sfxTing"><source src="ting.mp3" type="audio/mpeg"></audio>
<audio id="sfxChime"><source src="chime.mp3" type="audio/mpeg"></audio>

<div id="central-scoreboard">
    <span id="t1-score-top" class="score-num">0</span>
    <span style="font-size:32px; color:var(--dark);">-</span>
    <span id="t2-score-top" class="score-num">0</span>
</div>

<div id="main-menu" class="screen active">
    <div class="menu-header">
        <h1 class="main-title-gothic">𝔖𝔭𝔢𝔩𝔩𝔦𝔫𝔤 𝔊𝔞𝔪𝔢</h1>
        <div class="sub-title-clean">Teacher Khen Learning Hub</div>
    </div>
    <button class="menu-btn" onclick="playSfx('sfxTing'); showScreen('mode-screen')">START GAME<div class="shine"></div></button>
    <button class="menu-btn" onclick="playSfx('sfxTing'); showScreen('settings-screen')" style="background:var(--sec)">SETTINGS<div class="shine"></div></button>
</div>

<div id="mode-screen" class="screen">
    <h2 style="color:white; text-shadow: 2px 2px black;">SELECT MODE</h2>
    <button class="menu-btn" onclick="setMode('single')">SOLO MODE<div class="shine"></div></button>
    <button class="menu-btn" onclick="setMode('team')">TEAM VS MODE<div class="shine"></div></button>
</div>

<div id="char-screen" class="screen">
    <h2 id="char-title-display" style="color:white; text-shadow: 2px 2px black; font-size: 40px;">PLAYER 1 SELECT</h2>
    <div id="char-list" style="display:flex; gap:15px; margin-bottom:20px; flex-wrap:wrap; justify-content:center;"></div>
    <button class="menu-btn" onclick="confirmChar()">CONFIRM CHOICE<div class="shine"></div></button>
</div>

<div id="settings-screen" class="screen">
    <div class="menu-header" style="margin-bottom:10px">
        <h1 class="main-title-gothic" style="font-size:40px">𝔖𝔭𝔢𝔩𝔩𝔦𝔫𝔤 𝔊𝔞𝔪𝔢</h1>
        <div class="sub-title-clean" style="font-size:16px">Teacher Khen Learning Hub</div>
    </div>
    <div class="settings-content">
        <div class="grid">
            <div class="s-box">Timer: <input type="number" id="set-timer" value="60" style="width:70px"></div>
            <div class="s-box">Speed: <select id="set-speed"><option value="3">Slow</option><option value="5" selected>Normal</option><option value="8">Fast</option></select></div>
            <div class="s-box">Sync Qs: <input type="checkbox" id="set-sync" checked></div>
            <div class="s-box">HP Hearts: <input type="number" id="set-hp" value="5" style="width:50px"></div>
            <div class="s-box">Music: <input type="checkbox" id="set-music" checked onchange="updateAudio()"></div>
            <div class="s-box">Vol: <input type="range" id="set-vol" min="0" max="1" step="0.1" value="0.5" onchange="updateAudio()"></div>
            <div class="s-box">SFX: <input type="checkbox" id="set-sfx" checked></div>
            <div class="s-box" style="background:#e74c3c; color:white;">Clear App: <button onclick="clearAllData()" style="padding:5px 10px; cursor:pointer; font-weight:bold;">ERASE DATA</button></div>
        </div>
        <div style="margin: 20px 0; display:flex; gap:10px;">
            <button class="menu-btn" style="font-size:14px; background:#3498db" onclick="saveToFile()">💾 SAVE FILE</button>
            <button class="menu-btn" style="font-size:14px; background:#9b59b6" onclick="document.getElementById('f-in').click()">📂 LOAD FILE</button>
            <input type="file" id="f-in" style="display:none" onchange="loadFromFile(event)">
        </div>
        <table class="editor-table">
            <thead><tr><th>Hint / Image</th><th>Answer</th><th>Del</th></tr></thead>
            <tbody id="q-body"></tbody>
        </table>
        <button class="menu-btn" style="font-size:14px; background:#C4D7F2; color:#776871; margin-top:15px;" onclick="addRow()">+ ADD WORD</button>
        <button id="final-save-btn" class="menu-btn" style="background:var(--win); width:100%; margin-top:20px; height: 60px;" onclick="saveAndStart()">SAVE & START</button>
    </div>
</div>

<div id="gameplay-screen" class="screen">
    <div id="t1-side" class="game-half">
        <div class="hud hud-t1">
            <div id="t1-hp" style="font-size:22px"></div>
            <div style="display:flex; align-items:center; gap:20px;">
                <div id="t1-single-score" class="single-score-ui" style="display:none">0</div>
                <div id="t1-clock" style="font-size:24px; font-weight:bold;">60</div>
            </div>
        </div>
        <div class="question-area" id="t1-qarea">
            <div id="t1-hint"></div>
            <div id="t1-boxes" class="boxes"></div>
            <div id="t1-next-layer" class="completion-layer">
                <button class="menu-btn" style="background:var(--win); padding: 10px 15px; font-size: 14px;" onclick="proceedNext(0)">NEXT ➔</button>
            </div>
        </div>
        <div id="t1-lane" class="lane">
            <div class="fixed-controls">
                <button class="arrow-btn" onmousedown="startMove(0,-1)" onmouseup="stopMove(0)" ontouchstart="startMove(0,-1)" ontouchend="stopMove(0)">▲</button>
                <button class="arrow-btn" onmousedown="startMove(0,1)" onmouseup="stopMove(0)" ontouchstart="startMove(0,1)" ontouchend="stopMove(0)">▼</button>
            </div>
            <div id="t1-cont" class="char-container" style="top:40%"><img src="" id="t1-char" class="char"></div>
        </div>
        <div id="t1-over" class="game-over-layer"><h1>GAME OVER</h1><h2 id="t1-final-score"></h2><button class="menu-btn" onclick="resetSpecificTeam(0)">PLAY AGAIN</button><button class="menu-btn" style="background:#e74c3c" onclick="location.reload()">EXIT</button></div>
    </div>
    
    <div id="t2-side" class="game-half" style="display:none">
        <div class="hud hud-t2">
            <div id="t2-clock" style="font-size:24px; font-weight:bold;">60</div><div id="t2-hp" style="font-size:22px"></div>
        </div>
        <div class="question-area" id="t2-qarea">
            <div id="t2-hint"></div>
            <div id="t2-boxes" class="boxes"></div>
            <div id="t2-next-layer" class="completion-layer">
                <button class="menu-btn" style="background:var(--win); padding: 10px 15px; font-size: 14px;" onclick="proceedNext(1)">NEXT ➔</button>
            </div>
        </div>
        <div id="t2-lane" class="lane">
            <div class="fixed-controls">
                <button class="arrow-btn" onmousedown="startMove(1,-1)" onmouseup="stopMove(1)" ontouchstart="startMove(1,-1)" ontouchend="stopMove(1)">▲</button>
                <button class="arrow-btn" onmousedown="startMove(1,1)" onmouseup="stopMove(1)" ontouchstart="startMove(1,1)" ontouchend="stopMove(1)">▼</button>
            </div>
            <div id="t2-cont" class="char-container" style="top:40%"><img src="" id="t2-char" class="char"></div>
        </div>
        <div id="t2-over" class="game-over-layer"><h1>GAME OVER</h1><h2 id="t2-final-score"></h2><button class="menu-btn" onclick="resetSpecificTeam(1)">PLAY AGAIN</button><button class="menu-btn" style="background:#e74c3c" onclick="location.reload()">EXIT</button></div>
    </div>
</div>

<script>
const CHARS = ['pikachu.png', 'mario.gif', 'Luigi.gif', 'Sir Axiom.gif', 'character3.gif'];
const ALPHA_UP = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const ALPHA_LOW = "abcdefghijklmnopqrstuvwxyz";

let state = { mode:'single', pIndex:0, selected:[null, null], questions:[{q:'Fruit', a:'Apple', img:null}], timer:60, speed:5, sync:true, music:true, vol:0.5, sfx:true, hp:5 };
let game = { active:false, teams:[{hp:5, score:0, time:60, qIdx:0, typed:"", qList:[], alive:false, dy:0, letters:[], isPaused: false},{hp:5, score:0, time:60, qIdx:0, typed:"", qList:[], alive:false, dy:0, letters:[], isPaused: false}], lastFocus: null };

function playSfx(id) { 
    if(state.sfx) { 
        const s = document.getElementById(id);
        if(!s) return;
        s.volume = (id === 'sfxChime') ? 1.0 : state.vol;
        s.currentTime = 0; s.play().catch(()=>{});
    } 
}

async function handlePaste(e) {
    if(!document.getElementById('settings-screen').classList.contains('active')) return;
    const items = (e.clipboardData || e.originalEvent.clipboardData).items;
    for (let item of items) {
        if (item.type.indexOf("image") !== -1) {
            const reader = new FileReader();
            reader.onload = async (ev) => {
                const idx = game.lastFocus !== null ? game.lastFocus : state.questions.length - 1;
                state.questions[idx].img = ev.target.result; renderTable(); autoSave();
            };
            reader.readAsDataURL(item.getAsFile());
        }
    }
}

function renderTable() {
    const b = document.getElementById('q-body'); b.innerHTML = '';
    state.questions.forEach((q, i) => {
        const tr = document.createElement('tr');
        tr.innerHTML = `<td><input type="text" value="${q.q}" onfocus="game.lastFocus=${i}" oninput="state.questions[${i}].q=this.value;autoSave()">${q.img ? `<br><img src="${q.img}" style="height:40px;">` : ''}</td>
            <td><input type="text" value="${q.a}" onfocus="game.lastFocus=${i}" oninput="state.questions[${i}].a=this.value;autoSave()"></td>
            <td><button class="menu-btn" style="padding:5px 10px; background:#e74c3c" onclick="state.questions.splice(${i},1);renderTable();autoSave()">X</button></td>`;
        b.appendChild(tr);
    });
}

function addRow() { state.questions.push({q:'', a:'', img:null}); renderTable(); autoSave(); }
function autoSave() { try { localStorage.setItem('teacherKhen_Spelling_Pro', JSON.stringify(state)); } catch(e) {} }
function autoLoad() { const s = localStorage.getItem('teacherKhen_Spelling_Pro'); if(s) { state = JSON.parse(s); renderTable(); syncUI(); } }

function syncUI() {
    document.getElementById('set-timer').value = state.timer; document.getElementById('set-speed').value = state.speed;
    document.getElementById('set-sync').checked = state.sync; document.getElementById('set-hp').value = state.hp || 5;
    document.getElementById('set-music').checked = state.music; document.getElementById('set-vol').value = state.vol;
    document.getElementById('set-sfx').checked = state.sfx;
}

function saveAndStart() {
    state.timer = parseInt(document.getElementById('set-timer').value) || 60;
    state.speed = parseInt(document.getElementById('set-speed').value) || 5;
    state.sync = document.getElementById('set-sync').checked;
    state.hp = parseInt(document.getElementById('set-hp').value) || 5;
    state.sfx = document.getElementById('set-sfx').checked;
    autoSave(); showScreen('main-menu');
}

function showScreen(id){
    document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
    document.getElementById(id).classList.add('active');
    if(id === 'gameplay-screen') {
        document.getElementById('central-scoreboard').style.display = (state.mode === 'team') ? 'flex' : 'none';
        document.getElementById('t1-single-score').style.display = (state.mode === 'team') ? 'none' : 'block';
    } else { document.getElementById('central-scoreboard').style.display = 'none'; }
}

function setMode(m){ 
    state.mode=m; state.pIndex=0; state.selected=[null, null]; 
    document.getElementById('char-title-display').innerText = "PLAYER 1 SELECT";
    renderChars(); showScreen('char-screen'); 
}

function renderChars(){
    const l = document.getElementById('char-list'); l.innerHTML = '';
    CHARS.forEach(c => {
        const i = document.createElement('img'); i.src = c; i.style.width = '100px'; 
        i.style.border = (state.selected[state.pIndex]===c?'6px solid var(--win)':'2px solid white');
        i.style.borderRadius = "15px"; i.style.padding = "5px"; i.style.background = "rgba(255,255,255,0.3)";
        i.onclick = () => { playSfx('sfxChime'); state.selected[state.pIndex]=c; renderChars(); }; 
        l.appendChild(i);
    });
}

function confirmChar(){
    if(!state.selected[state.pIndex]) return;
    playSfx('sfxTing');
    if(state.mode==='team' && state.pIndex===0) { 
        state.pIndex=1; 
        document.getElementById('char-title-display').innerText = "PLAYER 2 SELECT";
        renderChars(); 
    }
    else { showScreen('gameplay-screen'); startGame(); }
}

function startGame(){
    game.active = true;
    document.getElementById('t2-side').style.display = (state.mode === 'team') ? 'flex' : 'none';
    initTeam(0); if(state.mode==='team') initTeam(1);
    updateAudio(); tick(); startClock();
    spawnLoop(0); if(state.mode==='team') spawnLoop(1);
}

function initTeam(idx){
    const t = game.teams[idx]; t.hp=state.hp; t.score=0; t.time=state.timer; t.qIdx=0; t.typed=""; t.alive=true; t.letters=[]; t.isPaused = false;
    t.qList = [...state.questions];
    if(!state.sync && idx===1) t.qList.sort(()=>Math.random()-0.8); else t.qList.sort(()=>Math.random()-0.5);
    document.getElementById(`t${idx+1}-score-top`).innerText = '0';
    if(idx===0) document.getElementById('t1-single-score').innerText = '0';
    document.getElementById(`t${idx+1}-char`).src = state.selected[idx];
    document.getElementById(`t${idx+1}-over`).style.display = 'none';
    document.getElementById(`t${idx+1}-next-layer`).style.display = 'none';
    updateHUD(idx); nextQ(idx);
}

function updateHUD(idx){
    const hp = document.getElementById(`t${idx+1}-hp`); hp.innerHTML = '';
    for(let i=0; i<game.teams[idx].hp; i++) hp.innerHTML += '❤️';
    document.getElementById(`t${idx+1}-clock`).innerText = game.teams[idx].time;
}

function nextQ(idx){
    const t = game.teams[idx]; 
    if(t.qIdx >= t.qList.length) { endGame(idx); return; }
    const q = t.qList[t.qIdx], hint = document.getElementById(`t${idx+1}-hint`);
    hint.innerHTML = q.img ? `<img src="${q.img}" class="question-img">` : `<div class="question-text">${q.q}</div>`;
    const b = document.getElementById(`t${idx+1}-boxes`); b.innerHTML = '';
    for(let i=0; i<q.a.length; i++) b.innerHTML += '<div class="box"></div>';
}

function spawnLoop(idx){
    if(!game.active || !game.teams[idx].alive) return;
    if(!game.teams[idx].isPaused){
        const lane = document.getElementById(`t${idx+1}-lane`);
        const el = document.createElement('div'); el.className = 'letter';
        const targetWord = game.teams[idx].qList[game.teams[idx].qIdx].a;
        const nextChar = targetWord[game.teams[idx].typed.length];
        
        if (Math.random() < 0.35) {
            el.innerText = nextChar;
        } else {
            const pool = (nextChar === nextChar.toUpperCase() && nextChar !== nextChar.toLowerCase()) ? ALPHA_UP : ALPHA_LOW;
            el.innerText = pool[Math.floor(Math.random()*pool.length)];
        }
        
        const startX = lane.offsetWidth; const startY = Math.random() * (lane.offsetHeight - 70);
        el.style.left = startX + 'px'; el.style.top = startY + 'px';
        lane.appendChild(el);
        game.teams[idx].letters.push({ dom: el, x: startX, y: startY, char: el.innerText });
    }
    // ADJUSTED: Wait longer between spawns to give more space for the character to move
    setTimeout(() => spawnLoop(idx), 1800);
}

function tick(){
    if(!game.active) return;
    game.teams.forEach((t, i) => {
        if(!t.alive) return;
        const cont = document.getElementById(`t${i+1}-cont`);
        if(!t.isPaused && t.dy !== 0){
            let top = cont.offsetTop + t.dy;
            cont.style.top = Math.max(0, Math.min(top, cont.parentElement.offsetHeight - 110)) + 'px';
        }
        const charRect = cont.getBoundingClientRect();
        for(let j=t.letters.length-1; j>=0; j--){
            const letObj = t.letters[j];
            if(!t.isPaused) letObj.x -= state.speed;
            letObj.dom.style.left = letObj.x + 'px';
            const letRect = letObj.dom.getBoundingClientRect();
            
            if(!t.isPaused && letRect.left < charRect.right && letRect.right > charRect.left && letRect.top < charRect.bottom && letRect.bottom > charRect.top){
                const target = t.qList[t.qIdx].a;
                if(letObj.char === target[t.typed.length]){
                    playSfx('sfxCorrect');
                    document.getElementById(`t${i+1}-boxes`).children[t.typed.length].innerText = letObj.char;
                    t.typed += letObj.char;
                    if(t.typed === target){
                        t.isPaused = true; cont.classList.add('paused');
                        document.getElementById(`t${i+1}-boxes`).innerHTML = `<div class="final-word">${target}</div>`;
                        playSfx('sfxNext'); t.score++; 
                        document.getElementById(`t${i+1}-score-top`).innerText = t.score;
                        if(i===0) document.getElementById('t1-single-score').innerText = t.score;
                        document.getElementById(`t${i+1}-next-layer`).style.display = 'flex';
                    }
                } else { playSfx('sfxWrong'); t.hp--; updateHUD(i); if(t.hp <= 0) endGame(i); }
                letObj.dom.remove(); t.letters.splice(j, 1);
            } else if(letObj.x < -80) { letObj.dom.remove(); t.letters.splice(j, 1); }
        }
    });
    requestAnimationFrame(tick);
}

function startClock(){
    const int = setInterval(() => {
        if(!game.active) return clearInterval(int);
        game.teams.forEach((t, i) => { if(t.alive && !t.isPaused) { t.time--; updateHUD(i); if(t.time <= 0) endGame(i); } });
    }, 1000);
}

function endGame(idx){
    game.teams[idx].alive = false;
    document.getElementById(`t${idx+1}-over`).style.display = 'flex';
    document.getElementById(`t${idx+1}-final-score`).innerText = "Final Score: " + game.teams[idx].score;
}

function startMove(idx, dir) { if(!game.teams[idx].isPaused && game.teams[idx].alive) game.teams[idx].dy = dir * 28; }
function stopMove(idx) { game.teams[idx].dy = 0; }

function proceedNext(idx) {
    playSfx('sfxTing');
    const t = game.teams[idx];
    document.getElementById(`t${idx+1}-cont`).classList.remove('paused');
    document.getElementById(`t${idx+1}-next-layer`).style.display = 'none';
    t.isPaused = false; t.qIdx++; t.typed=""; nextQ(idx);
}

function updateAudio(){ 
    const m=document.getElementById('bgMusic'); m.volume = state.vol = document.getElementById('set-vol').value;
    state.music = document.getElementById('set-music').checked;
    if(state.music && game.active) m.play().catch(()=>{}); else m.pause();
}

function saveToFile(){ const b=new Blob([JSON.stringify(state)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(b); a.download='Lesson.json'; a.click(); }
function loadFromFile(e){ const r=new FileReader(); r.onload=(ev)=>{ state=JSON.parse(ev.target.result); renderTable(); syncUI(); autoSave(); }; r.readAsText(e.target.files[0]); }
function clearAllData() { if(confirm("Erase all data?")) { localStorage.removeItem('teacherKhen_Spelling_Pro'); location.reload(); } }
autoLoad();
</script>
</body>
</html>
Teacher Khen Learning Hub

Post a Comment

0 Comments