Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>JS Coding Game - Ultra Advanced Template</title> | |
| <!-- CodeMirror CSS & JS --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script> | |
| <!-- Matter.js Library --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script> | |
| <style> | |
| body { font-family: Arial, sans-serif; padding: 10px; } | |
| #editor { width: 100%; height: 200px; } | |
| #runBtn, #stopBtn { padding: 8px 15px; margin-top: 5px; margin-right: 5px; cursor: pointer; } | |
| #output { background: black; color: lime; padding: 10px; height: 100px; overflow-y: auto; font-family: monospace; } | |
| #gameCanvas { border: 1px solid black; background: lightblue; display: block; margin-top: 10px; } | |
| #guide { background: #f4f4f4; border: 1px solid #ccc; padding: 10px; margin-bottom: 15px; } | |
| #status { margin-top: 10px; font-family: monospace; } | |
| h3, h4 { margin: 5px 0; } | |
| .cooldown { color: gray; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- How To Play Guide --> | |
| <div id="guide"> | |
| <h3>How to Play</h3> | |
| <p> | |
| Write JavaScript in the editor to control the hero in a dynamic world. Define an <code>update()</code> function that runs every 100ms when you click "Run Code." Use "Stop" to pause. | |
| </p> | |
| <h4>Commands (with Cooldowns)</h4> | |
| <ul> | |
| <li><code>console.log("move 5");</code> β Sets x-velocity (no cooldown).</li> | |
| <li><code>console.log("jump 0.05");</code> β Jumping force (0.5s cooldown).</li> | |
| <li><code>console.log("attack sword");</code> β Sword attack (1s cooldown).</li> | |
| <li><code>console.log("attack bow");</code> β Fires arrow (2s cooldown).</li> | |
| <li><code>console.log("attack bomb");</code> β Throws bomb (3s cooldown).</li> | |
| <li><code>console.log("heal 20");</code> β Heals hero (5s cooldown, requires potion).</li> | |
| </ul> | |
| <h4>Advanced Features</h4> | |
| <p>Create continuous logic:</p> | |
| <pre style="background:#eee; padding:10px;"> | |
| function update() { | |
| if (hero.position.x < portal.position.x) console.log("move 5"); | |
| if (enemies.length > 0 && hero.inventory.potions > 0 && hero.health < 50) console.log("heal 20"); | |
| } | |
| </pre> | |
| <p>Key variables:</p> | |
| <ul> | |
| <li><code>hero</code> β Hero body (<code>.health</code>, <code>.inventory</code>, <code>.abilities</code>).</li> | |
| <li><code>enemies</code> β Array of enemies (incl. boss).</li> | |
| <li><code>items</code> β Array of collectibles.</li> | |
| <li><code>portal</code> β Exit portal body.</li> | |
| <li><code>obstacles</code> β Array of hazards.</li> | |
| <li><code>engine</code>, <code>world</code> β Matter.js instances.</li> | |
| </ul> | |
| </div> | |
| <!-- Task Display --> | |
| <h4>Task: <span id="taskText"></span></h4> | |
| <!-- Game Status --> | |
| <div id="status"> | |
| <p>Health: <span id="health">100</span> | Score: <span id="score">0</span> | Potions: <span id="potions">0</span></p> | |
| <p>Cooldowns: Jump: <span id="cdJump">Ready</span> | Sword: <span id="cdSword">Ready</span> | Bow: <span id="cdBow">Ready</span> | Bomb: <span id="cdBomb">Ready</span> | Heal: <span id="cdHeal">Ready</span></p> | |
| </div> | |
| <!-- Code Editor --> | |
| <textarea id="editor"> | |
| // Continuous game logic to clear first level (Collect a potion) | |
| function update() { | |
| // Target the first potion at x=200 | |
| const targetX = 200; | |
| // Move toward the potion | |
| if (hero.position.x < targetX - 10) { | |
| console.log("move 5"); | |
| } else if (hero.position.x > targetX + 10) { | |
| console.log("move -5"); | |
| } else { | |
| console.log("move 0"); // Stop near the potion | |
| } | |
| // Jump over obstacles if close | |
| obstacles.forEach(obstacle => { | |
| const distanceX = Math.abs(hero.position.x - obstacle.position.x); | |
| const distanceY = hero.position.y - obstacle.position.y; | |
| if (distanceX < 50 && distanceY > 0) { | |
| console.log("jump 0.05"); | |
| } | |
| }); | |
| // Stop once potion is collected | |
| if (hero.inventory.potions > 0) { | |
| console.log("move 0"); | |
| } | |
| } | |
| </textarea> | |
| <br> | |
| <button id="runBtn">Run Code</button> | |
| <button id="stopBtn">Stop</button> | |
| <!-- Console Output --> | |
| <h4>Output:</h4> | |
| <div id="output"></div> | |
| <!-- Game Canvas --> | |
| <canvas id="gameCanvas"></canvas> | |
| <script> | |
| // --- Setup CodeMirror Editor --- | |
| const editor = CodeMirror.fromTextArea(document.getElementById("editor"), { | |
| mode: "javascript", | |
| lineNumbers: true | |
| }); | |
| // --- Matter.js Game Setup --- | |
| const Engine = Matter.Engine, | |
| Render = Matter.Render, | |
| World = Matter.World, | |
| Bodies = Matter.Bodies, | |
| Body = Matter.Body, | |
| Events = Matter.Events; | |
| const engine = Engine.create(); | |
| const render = Render.create({ | |
| canvas: document.getElementById("gameCanvas"), | |
| engine: engine, | |
| options: { width: 800, height: 400, wireframes: false, background: '#87CEEB' } | |
| }); | |
| // Collision Categories | |
| const categoryHero = 0x0001; | |
| const categoryEnemy = 0x0002; | |
| const categoryProjectile = 0x0004; | |
| const categoryWeapon = 0x0008; | |
| const categoryItem = 0x0010; | |
| const categoryStatic = 0x0020; | |
| const categoryHazard = 0x0040; | |
| // --- Procedural Level Generation --- | |
| const ground = Bodies.rectangle(400, 390, 800, 20, { | |
| isStatic: true, | |
| render: { fillStyle: 'brown' }, | |
| collisionFilter: { category: categoryStatic } | |
| }); | |
| const obstacles = []; | |
| for (let i = 0; i < 5; i++) { | |
| const x = 150 + i * 150; | |
| const y = 350 - Math.random() * 100; | |
| obstacles.push(Bodies.rectangle(x, y, 50, 20, { | |
| isStatic: true, | |
| render: { fillStyle: 'gray' }, | |
| collisionFilter: { category: categoryHazard, mask: categoryHero | categoryEnemy } | |
| })); | |
| } | |
| const hero = Bodies.rectangle(50, 350, 40, 40, { | |
| restitution: 0.5, | |
| render: { fillStyle: 'blue' }, | |
| collisionFilter: { category: categoryHero, mask: 0xFFFF } | |
| }); | |
| hero.health = 100; | |
| hero.inventory = { potions: 0 }; | |
| hero.abilities = { | |
| jump: { cooldown: 500, lastUsed: 0 }, | |
| sword: { cooldown: 1000, lastUsed: 0 }, | |
| bow: { cooldown: 2000, lastUsed: 0 }, | |
| bomb: { cooldown: 3000, lastUsed: 0 }, | |
| heal: { cooldown: 5000, lastUsed: 0 } | |
| }; | |
| const sword = Bodies.rectangle(90, 350, 30, 10, { | |
| isStatic: true, | |
| render: { visible: false, fillStyle: 'silver' }, | |
| collisionFilter: { category: categoryWeapon } | |
| }); | |
| let arrows = []; | |
| let bombs = []; | |
| let enemyProjectiles = []; | |
| const enemies = [ | |
| Bodies.rectangle(300, 350, 40, 40, { | |
| render: { fillStyle: 'red' }, | |
| collisionFilter: { category: categoryEnemy } | |
| }), | |
| Bodies.rectangle(450, 350, 40, 40, { | |
| render: { fillStyle: 'red' }, | |
| collisionFilter: { category: categoryEnemy } | |
| }), | |
| Bodies.rectangle(700, 350, 60, 60, { | |
| render: { fillStyle: 'purple' }, | |
| collisionFilter: { category: categoryEnemy }, | |
| isBoss: true, health: 50 | |
| }) | |
| ]; | |
| enemies.forEach(e => e.isEnemy = true); | |
| const items = [ | |
| Bodies.circle(200, 300, 10, { | |
| isStatic: true, | |
| render: { fillStyle: 'green' }, | |
| collisionFilter: { category: categoryItem }, | |
| isPotion: true | |
| }), | |
| Bodies.circle(500, 300, 10, { | |
| isStatic: true, | |
| render: { fillStyle: 'green' }, | |
| collisionFilter: { category: categoryItem }, | |
| isPotion: true | |
| }) | |
| ]; | |
| const portal = Bodies.rectangle(750, 350, 40, 40, { | |
| isStatic: true, | |
| render: { fillStyle: 'cyan' }, | |
| collisionFilter: { category: categoryStatic }, | |
| isPortal: true | |
| }); | |
| let score = 0; | |
| World.add(engine.world, [ground, ...obstacles, hero, sword, ...enemies, ...items, portal]); | |
| Engine.run(engine); | |
| Render.run(render); | |
| // --- Expose Variables Globally --- | |
| window.engine = engine; | |
| window.world = engine.world; | |
| window.hero = hero; | |
| window.enemies = enemies; | |
| window.items = items; | |
| window.obstacles = obstacles; | |
| window.portal = portal; | |
| window.sword = sword; | |
| // --- Task System --- | |
| const tasks = [ | |
| { id: 1, text: "Collect a potion", condition: () => hero.inventory.potions > 0 }, | |
| { id: 2, text: "Defeat 2 regular enemies", condition: () => enemies.filter(e => !e.isBoss && e.isRemoved).length >= 2 }, | |
| { id: 3, text: "Defeat the boss (purple enemy)", condition: () => enemies.filter(e => e.isBoss)[0].isRemoved }, | |
| { id: 4, text: "Reach the portal with all enemies defeated", condition: () => enemies.every(e => e.isRemoved) && Matter.Bounds.overlaps(hero.bounds, portal.bounds) } | |
| ]; | |
| let currentTaskIndex = 0; | |
| function updateTaskText() { | |
| document.getElementById("taskText").innerText = tasks[currentTaskIndex].text; | |
| } | |
| updateTaskText(); | |
| // --- Advanced Enemy AI --- | |
| function moveEnemies() { | |
| enemies.forEach(enemy => { | |
| if (enemy.isRemoved) return; | |
| const dx = hero.position.x - enemy.position.x; | |
| const speed = enemy.isBoss ? 2 : 1.5; | |
| Body.setVelocity(enemy, { x: dx > 0 ? speed : -speed, y: enemy.velocity.y }); | |
| if (Math.random() < (enemy.isBoss ? 0.02 : 0.01)) Body.applyForce(enemy, enemy.position, { x: 0, y: -0.05 }); | |
| if (Math.random() < (enemy.isBoss ? 0.02 : 0.005)) enemyFireProjectile(enemy); | |
| }); | |
| } | |
| setInterval(moveEnemies, 100); | |
| function enemyFireProjectile(enemy) { | |
| const projectile = Bodies.circle(enemy.position.x, enemy.position.y, enemy.isBoss ? 10 : 5, { | |
| restitution: 0.2, | |
| render: { fillStyle: enemy.isBoss ? 'magenta' : 'orange' }, | |
| collisionFilter: { category: categoryProjectile }, | |
| isProjectile: true, | |
| damage: enemy.isBoss ? 20 : 10 | |
| }); | |
| const dx = hero.position.x - enemy.position.x; | |
| const dy = hero.position.y - enemy.position.y; | |
| const angle = Math.atan2(dy, dx); | |
| Body.setVelocity(projectile, { x: 5 * Math.cos(angle), y: 5 * Math.sin(angle) }); | |
| enemyProjectiles.push(projectile); | |
| World.add(engine.world, projectile); | |
| setTimeout(() => { | |
| enemyProjectiles = enemyProjectiles.filter(p => p !== projectile); | |
| World.remove(engine.world, projectile); | |
| }, 3000); | |
| } | |
| // --- Command Handlers with Cooldowns --- | |
| function customConsoleLog(...args) { | |
| const outputDiv = document.getElementById("output"); | |
| outputDiv.innerHTML += args.join(" ") + "<br>"; | |
| handleGameAction(args.join(" ")); | |
| } | |
| function handleGameAction(command) { | |
| const parts = command.split(" "); | |
| const action = parts[0]; | |
| const param = parts[1]; | |
| const value = parseFloat(parts[1]); | |
| const now = Date.now(); | |
| function isCooldownReady(ability) { | |
| return now - hero.abilities[ability].lastUsed >= hero.abilities[ability].cooldown; | |
| } | |
| function updateCooldownDisplay(ability, ready) { | |
| document.getElementById(`cd${ability.charAt(0).toUpperCase() + ability.slice(1)}`).innerText = ready ? "Ready" : `${((hero.abilities[ability].lastUsed + hero.abilities[ability].cooldown - now) / 1000).toFixed(1)}s`; | |
| } | |
| if (action === "move" && !isNaN(value)) { | |
| Body.setVelocity(hero, { x: value, y: hero.velocity.y }); | |
| } else if (action === "jump" && !isNaN(value) && isCooldownReady("jump")) { | |
| Body.applyForce(hero, hero.position, { x: 0, y: -value }); | |
| hero.abilities.jump.lastUsed = now; | |
| updateCooldownDisplay("jump", false); | |
| } else if (action === "attack") { | |
| if (param === "sword" && isCooldownReady("sword")) { | |
| performSwordAttack(); | |
| hero.abilities.sword.lastUsed = now; | |
| updateCooldownDisplay("sword", false); | |
| } else if (param === "bow" && isCooldownReady("bow")) { | |
| fireArrow(); | |
| hero.abilities.bow.lastUsed = now; | |
| updateCooldownDisplay("bow", false); | |
| } else if (param === "bomb" && isCooldownReady("bomb")) { | |
| throwBomb(); | |
| hero.abilities.bomb.lastUsed = now; | |
| updateCooldownDisplay("bomb", false); | |
| } | |
| } else if (action === "heal" && !isNaN(value) && isCooldownReady("heal") && hero.inventory.potions > 0) { | |
| hero.health = Math.min(100, hero.health + value); | |
| hero.inventory.potions--; | |
| hero.abilities.heal.lastUsed = now; | |
| document.getElementById("health").innerText = hero.health; | |
| document.getElementById("potions").innerText = hero.inventory.potions; | |
| updateCooldownDisplay("heal", false); | |
| } | |
| } | |
| function performSwordAttack() { | |
| Body.setPosition(sword, { x: hero.position.x + 30, y: hero.position.y }); | |
| sword.render.visible = true; | |
| enemies.forEach(enemy => { | |
| if (!enemy.isRemoved && Matter.Bounds.overlaps(sword.bounds, enemy.bounds)) { | |
| if (enemy.isBoss) { | |
| enemy.health -= 10; | |
| if (enemy.health <= 0) { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 50; | |
| } | |
| } else { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 10; | |
| } | |
| document.getElementById("score").innerText = score; | |
| } | |
| }); | |
| setTimeout(() => sword.render.visible = false, 500); | |
| } | |
| function fireArrow() { | |
| const arrow = Bodies.rectangle(hero.position.x + 20, hero.position.y, 20, 5, { | |
| restitution: 0.2, | |
| render: { fillStyle: 'yellow' }, | |
| collisionFilter: { category: categoryWeapon }, | |
| isArrow: true | |
| }); | |
| Body.setVelocity(arrow, { x: 5, y: 0 }); | |
| arrows.push(arrow); | |
| World.add(engine.world, arrow); | |
| setTimeout(() => { | |
| arrows = arrows.filter(a => a !== arrow); | |
| World.remove(engine.world, arrow); | |
| }, 3000); | |
| } | |
| function throwBomb() { | |
| const bomb = Bodies.circle(hero.position.x, hero.position.y - 10, 10, { | |
| restitution: 0.5, | |
| render: { fillStyle: 'black' }, | |
| collisionFilter: { category: categoryWeapon } | |
| }); | |
| Body.setVelocity(bomb, { x: 3, y: -3 }); | |
| bombs.push(bomb); | |
| World.add(engine.world, bomb); | |
| setTimeout(() => { | |
| bombs = bombs.filter(b => b !== bomb); | |
| World.remove(engine.world, bomb); | |
| enemies.forEach(enemy => { | |
| if (!enemy.isRemoved && Matter.Bounds.overlaps(bomb.bounds, enemy.bounds)) { | |
| if (enemy.isBoss) { | |
| enemy.health -= 20; | |
| if (enemy.health <= 0) { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 50; | |
| } | |
| } else { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 10; | |
| } | |
| document.getElementById("score").innerText = score; | |
| } | |
| }); | |
| }, 2000); | |
| } | |
| // --- Collision Handling --- | |
| Events.on(engine, 'collisionStart', function(event) { | |
| const pairs = event.pairs; | |
| pairs.forEach(pair => { | |
| const bodyA = pair.bodyA; | |
| const bodyB = pair.bodyB; | |
| // Hero vs Enemy | |
| if ((bodyA === hero && bodyB.isEnemy) || (bodyB === hero && bodyA.isEnemy)) { | |
| hero.health -= bodyA.isBoss || bodyB.isBoss ? 30 : 20; | |
| checkHeroHealth(); | |
| } | |
| // Hero vs Enemy Projectile | |
| if ((bodyA === hero && bodyB.isProjectile) || (bodyB === hero && bodyA.isProjectile)) { | |
| const projectile = bodyA.isProjectile ? bodyA : bodyB; | |
| hero.health -= projectile.damage; | |
| World.remove(engine.world, projectile); | |
| enemyProjectiles = enemyProjectiles.filter(p => p !== projectile); | |
| checkHeroHealth(); | |
| } | |
| // Enemy vs Hero Weapon | |
| if ((bodyA.isArrow && bodyB.isEnemy) || (bodyB.isArrow && bodyA.isEnemy)) { | |
| const arrow = bodyA.isArrow ? bodyA : bodyB; | |
| const enemy = bodyA.isEnemy ? bodyA : bodyB; | |
| if (enemy.isBoss) { | |
| enemy.health -= 5; | |
| if (enemy.health <= 0) { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 50; | |
| } | |
| } else { | |
| World.remove(engine.world, enemy); | |
| enemy.isRemoved = true; | |
| score += 10; | |
| } | |
| World.remove(engine.world, arrow); | |
| arrows = arrows.filter(a => a !== arrow); | |
| document.getElementById("score").innerText = score; | |
| } | |
| // Hero vs Item | |
| if ((bodyA === hero && bodyB.isPotion) || (bodyB === hero && bodyA.isPotion)) { | |
| const potion = bodyA === hero ? bodyB : bodyA; | |
| hero.inventory.potions++; | |
| World.remove(engine.world, potion); | |
| items.splice(items.indexOf(potion), 1); | |
| document.getElementById("potions").innerText = hero.inventory.potions; | |
| } | |
| // Hero vs Obstacle (Hazard) | |
| if ((bodyA === hero && bodyB.collisionFilter.category === categoryHazard) || (bodyB === hero && bodyA.collisionFilter.category === categoryHazard)) { | |
| hero.health -= 5; | |
| checkHeroHealth(); | |
| } | |
| }); | |
| }); | |
| function checkHeroHealth() { | |
| document.getElementById("health").innerText = hero.health; | |
| if (hero.health <= 0) { | |
| alert("Game Over! Hero defeated."); | |
| resetGame(); | |
| } | |
| } | |
| // --- Game Logic --- | |
| function checkTaskCompletion() { | |
| if (tasks[currentTaskIndex].condition()) { | |
| alert(`β Task Completed: ${tasks[currentTaskIndex].text}`); | |
| currentTaskIndex++; | |
| if (currentTaskIndex < tasks.length) { | |
| updateTaskText(); | |
| resetGame(false); | |
| } else { | |
| alert("π Victory! Youβve conquered the level!"); | |
| } | |
| } | |
| } | |
| function resetGame(fullReset = true) { | |
| hero.health = 100; | |
| Body.setPosition(hero, { x: 50, y: 350 }); | |
| Body.setVelocity(hero, { x: 0, y: 0 }); | |
| document.getElementById("health").innerText = hero.health; | |
| if (fullReset) { | |
| hero.inventory.potions = 0; | |
| score = 0; | |
| document.getElementById("score").innerText = score; | |
| document.getElementById("potions").innerText = hero.inventory.potions; | |
| enemies.forEach(e => e.isRemoved && World.remove(engine.world, e)); | |
| enemies.length = 0; | |
| enemies.push(...[ | |
| Bodies.rectangle(300, 350, 40, 40, { render: { fillStyle: 'red' }, collisionFilter: { category: categoryEnemy } }), | |
| Bodies.rectangle(450, 350, 40, 40, { render: { fillStyle: 'red' }, collisionFilter: { category: categoryEnemy } }), | |
| Bodies.rectangle(700, 350, 60, 60, { render: { fillStyle: 'purple' }, collisionFilter: { category: categoryEnemy }, isBoss: true, health: 50 }) | |
| ]); | |
| enemies.forEach(e => e.isEnemy = true); | |
| World.add(engine.world, enemies); | |
| items.length = 0; | |
| items.push(...[ | |
| Bodies.circle(200, 300, 10, { isStatic: true, render: { fillStyle: 'green' }, collisionFilter: { category: categoryItem }, isPotion: true }), | |
| Bodies.circle(500, 300, 10, { isStatic: true, render: { fillStyle: 'green' }, collisionFilter: { category: categoryItem }, isPotion: true }) | |
| ]); | |
| World.add(engine.world, items); | |
| currentTaskIndex = 0; | |
| updateTaskText(); | |
| } | |
| } | |
| // --- Continuous Code Execution --- | |
| let updateInterval; | |
| document.getElementById("runBtn").addEventListener("click", () => { | |
| if (updateInterval) clearInterval(updateInterval); | |
| document.getElementById("output").innerHTML = ""; | |
| const userCode = editor.getValue(); | |
| try { | |
| const oldConsoleLog = console.log; | |
| console.log = customConsoleLog; | |
| const userFunction = new Function(userCode + '; return update;'); | |
| const updateFn = userFunction(); | |
| if (typeof updateFn === 'function') { | |
| updateInterval = setInterval(() => { | |
| try { | |
| updateFn(); | |
| checkTaskCompletion(); | |
| const now = Date.now(); | |
| ['jump', 'sword', 'bow', 'bomb', 'heal'].forEach(ability => { | |
| if (now - hero.abilities[ability].lastUsed >= hero.abilities[ability].cooldown) { | |
| document.getElementById(`cd${ability.charAt(0).toUpperCase() + ability.slice(1)}`).innerText = "Ready"; | |
| } | |
| }); | |
| } catch (error) { | |
| document.getElementById("output").innerHTML += 'Error in update: ' + error.message + '<br>'; | |
| } | |
| }, 100); | |
| } | |
| console.log = oldConsoleLog; | |
| } catch (error) { | |
| document.getElementById("output").innerHTML = "Error: " + error.message; | |
| } | |
| }); | |
| document.getElementById("stopBtn").addEventListener("click", () => { | |
| if (updateInterval) clearInterval(updateInterval); | |
| }); | |
| </script> | |
| </body> | |
| </html> |