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> |