|
<!doctype html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<meta name="viewport" content="width=device-width,initial-scale=1" /> |
|
<title>Sling Humans β Prototype</title> |
|
<style> |
|
html,body{height:100%;margin:0;background:linear-gradient(#bfe9ff,#eaf7ff);font-family:Inter,system-ui,Segoe UI,Arial} |
|
#game {width:100%;height:100vh;overflow:hidden} |
|
.ui {position:absolute;left:14px;top:14px;background:rgba(255,255,255,0.9);padding:10px;border-radius:10px;box-shadow:0 6px 18px rgba(13,38,76,0.08);z-index:10} |
|
.ui button{background:#1976d2;color:white;border:none;padding:8px 12px;border-radius:8px;cursor:pointer} |
|
.ui .small{font-size:13px;color:#0b2540;margin-top:6px} |
|
.credits{position:absolute;right:14px;top:14px;background:rgba(255,255,255,0.9);padding:8px 10px;border-radius:8px;font-size:13px;z-index:10} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="game"></div> |
|
<div class="ui"> |
|
<div style="font-weight:700">Sling Humans β Prototype</div> |
|
<div style="margin-top:8px;display:flex;gap:8px"> |
|
<button id="restart">Restart</button> |
|
<button id="next">Next Level</button> |
|
<button id="powerup">Mega Launch</button> |
|
</div> |
|
<div class="small">Score: <span id="score">0</span> β’ Ammo: <span id="ammo">5</span> β’ Stars: <span id="stars">0</span></div> |
|
<div style="margin-top:8px;font-size:13px;color:#444">Human: <select id="hType"> |
|
<option value="regular">Regular</option> |
|
<option value="heavy">Heavyweight</option> |
|
<option value="jumper">Jumper</option> |
|
<option value="boomer">Boomer</option> |
|
</select></div> |
|
</div> |
|
<div class="credits">Cartoon style β non graphic</div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script> |
|
<script> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const WIDTH = Math.min(window.innerWidth, 1200); |
|
const HEIGHT = Math.min(window.innerHeight, 800); |
|
|
|
const config = { |
|
type: Phaser.AUTO, |
|
parent: 'game', |
|
width: WIDTH, |
|
height: HEIGHT, |
|
backgroundColor: 0xe8f7ff, |
|
physics: { default: 'matter', matter: { gravity: { y: 1.0 }, debug: false } }, |
|
scene: { preload, create, update } |
|
}; |
|
|
|
const game = new Phaser.Game(config); |
|
|
|
let slingPos = { x: 160, y: HEIGHT - 180 }; |
|
let currentHuman = null, isDragging=false; |
|
let dottedLines = []; |
|
let score = 0, ammo = 5, level = 0, stars = 0; |
|
let blocksGroup = [], targetsGroup = []; |
|
let megaLaunchActive = false; |
|
|
|
function preload() { |
|
|
|
} |
|
|
|
function create() { |
|
const scene = this; |
|
|
|
|
|
const ground = scene.matter.add.rectangle(WIDTH/2, HEIGHT-24, WIDTH, 48, { isStatic: true, label: 'ground' }); |
|
|
|
|
|
scene.add.circle(slingPos.x, slingPos.y, 20, 0x6d4c41).setDepth(2); |
|
scene.add.rectangle(slingPos.x+28, slingPos.y+12, 10, 40, 0x4e342e).setOrigin(0.5).setDepth(2); |
|
|
|
|
|
document.getElementById('restart').onclick = () => resetLevel(scene); |
|
document.getElementById('next').onclick = () => nextLevel(scene); |
|
document.getElementById('powerup').onclick = () => { megaLaunchActive = true; document.getElementById('powerup').innerText='Mega! (On)'; setTimeout(()=>{megaLaunchActive=false; document.getElementById('powerup').innerText='Mega Launch';}, 8000); }; |
|
|
|
|
|
buildLevel(scene, level); |
|
|
|
|
|
spawnHuman(scene); |
|
|
|
|
|
scene.input.on('pointerdown', (pointer) => { |
|
if (!currentHuman) return; |
|
const bodies = Phaser.Physics.Matter.Matter.Query.point([currentHuman.body], pointer); |
|
if (bodies.length) { |
|
isDragging = true; |
|
scene.matter.body.setStatic(currentHuman.body, true); |
|
} |
|
}); |
|
|
|
scene.input.on('pointermove', (pointer) => { |
|
if (!isDragging || !currentHuman) return; |
|
|
|
const maxDrag = 250; |
|
const dx = pointer.x - slingPos.x; |
|
const dy = pointer.y - slingPos.y; |
|
const dist = Math.min(Math.hypot(dx,dy), maxDrag); |
|
const angle = Math.atan2(dy,dx); |
|
const px = slingPos.x + Math.cos(angle)*dist; |
|
const py = slingPos.y + Math.sin(angle)*dist; |
|
currentHuman.setPosition(px, py); |
|
drawTrajectory(scene, currentHuman.x, currentHuman.y, slingPos.x - px, slingPos.y - py); |
|
}); |
|
|
|
scene.input.on('pointerup', (pointer) => { |
|
if (!isDragging || !currentHuman) return; |
|
isDragging = false; |
|
clearTrajectory(); |
|
|
|
const dx = slingPos.x - currentHuman.x; |
|
const dy = slingPos.y - currentHuman.y; |
|
let power = Math.sqrt(dx*dx + dy*dy); |
|
const angle = Math.atan2(dy, dx); |
|
let velocityFactor = 0.08 * (megaLaunchActive ? 2.2 : 1.0); |
|
const vx = Math.cos(angle) * power * velocityFactor; |
|
const vy = Math.sin(angle) * power * velocityFactor; |
|
scene.matter.body.setStatic(currentHuman.body, false); |
|
scene.matter.body.setVelocity(currentHuman.body, { x: vx, y: vy }); |
|
|
|
scene.matter.body.setAngularVelocity(currentHuman.body, (Math.random()*6-3)); |
|
|
|
scene.cameras.main.startFollow(currentHuman, true, 0.06, 0.06); |
|
|
|
ammo = Math.max(0, ammo-1); |
|
updateUi(); |
|
|
|
scene.time.delayedCall(800, ()=>{ if (ammo>0) spawnHuman(scene); }); |
|
}); |
|
|
|
|
|
scene.matter.world.on('collisionstart', (event) => { |
|
event.pairs.forEach(pair => { |
|
const { bodyA, bodyB } = pair; |
|
[bodyA, bodyB].forEach(b => { |
|
if (!b.gameObject) return; |
|
const go = b.gameObject; |
|
|
|
if (go.humanType === 'boomer') { |
|
const speed = pair.collision.penetration && Math.hypot(pair.collision.penetration.x, pair.collision.penetration.y) || 0; |
|
if (speed > 1.2 && !go.exploded) { |
|
go.exploded = true; |
|
explodeAt(scene, go.x, go.y); |
|
scene.matter.world.remove(go.body); |
|
go.destroy(); |
|
} |
|
} |
|
}); |
|
|
|
const bodies = [bodyA.gameObject, bodyB.gameObject]; |
|
bodies.forEach(obj => { |
|
if (obj && obj.isBlock && !obj.scored) { |
|
|
|
const speed = obj.body.speed || 0; |
|
const angle = Math.abs(obj.rotation || 0); |
|
if (speed > 3 || angle > 0.6) { |
|
obj.scored = true; |
|
score += (obj.blockType==='glass' ? 30 : obj.blockType==='wood' ? 15 : 8); |
|
updateUi(); |
|
|
|
scene.tweens.add({ targets: obj, alpha: 0.3, duration: 200, yoyo: true }); |
|
} |
|
} |
|
}); |
|
}); |
|
}); |
|
|
|
|
|
scene.cameras.main.setBounds(0,0, WIDTH*1.5, HEIGHT); |
|
scene.cameras.main.setZoom(1.0); |
|
} |
|
|
|
function update() { |
|
|
|
const scene = this; |
|
|
|
if (targetsGroup.every(t=>t.destroyed)) { |
|
|
|
if (!this._cleared) { |
|
this._cleared = true; |
|
scene.cameras.main.fadeOut(600, 255,255,255); |
|
scene.time.delayedCall(700, ()=> { |
|
|
|
const blocksLeft = blocksGroup.filter(b=>!b.scored).length; |
|
stars = Math.min(3, Math.max(1, Math.floor(score/100)+1)); |
|
document.getElementById('stars').innerText = stars; |
|
alert('Level Cleared! Score: '+score+' Stars: '+stars); |
|
nextLevel(scene); |
|
}); |
|
} |
|
} |
|
|
|
if (ammo<=0 && !currentHuman) { |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
function spawnHuman(scene) { |
|
const type = document.getElementById('hType').value; |
|
const emoji = 'π§'; |
|
const container = scene.add.container(slingPos.x, slingPos.y); |
|
const circle = scene.add.circle(0,0,22, 0xffcc80).setStrokeStyle(2,0xdb8b3b); |
|
const txt = scene.add.text(-10,-16, emoji, { fontSize: '28px' }); |
|
container.add([circle, txt]); |
|
scene.matter.add.gameObject(container, { shape: { type: 'circle', radius: 22 }, label: 'human' }); |
|
container.setDepth(3); |
|
container.isHuman = true; |
|
container.humanType = type; |
|
|
|
const body = container.body; |
|
body.friction = 0.8; |
|
body.restitution = (type==='jumper' ? 0.8 : 0.2); |
|
body.density = (type==='heavy' ? 0.009 : type==='boomer' ? 0.002 : 0.004); |
|
|
|
if (type==='jumper') { |
|
container.onGroundBounce = true; |
|
} |
|
if (type==='boomer') { |
|
|
|
} |
|
|
|
currentHuman = container; |
|
|
|
scene.tweens.add({ targets: container, y: container.y-6, duration: 600, yoyo:true, repeat:-1 }); |
|
|
|
scene.matter.world.setCollisionCategory(0); |
|
|
|
blocksGroup.forEach(b => scene.matter.add.collider(container, b)); |
|
|
|
targetsGroup.forEach(t => scene.matter.add.collider(container, t)); |
|
|
|
updateUi(); |
|
} |
|
|
|
function buildLevel(scene, lvl) { |
|
|
|
blocksGroup.forEach(b=>{ try{ b.destroy(); }catch(e){} }); |
|
targetsGroup.forEach(t=>{ try{ t.destroy(); }catch(e){} }); |
|
blocksGroup = []; targetsGroup = []; |
|
|
|
|
|
const baseX = WIDTH - 300; |
|
const baseY = HEIGHT - 120; |
|
|
|
const pattern = 2 + (lvl % 3); |
|
for (let s=0; s<pattern; s++) { |
|
const cols = 2 + s; |
|
const rows = 2 + Math.floor(s/1); |
|
const startX = baseX + s*70; |
|
for (let i=0;i<cols;i++){ |
|
for (let j=0;j<rows;j++){ |
|
const x = startX + i*48; |
|
const y = baseY - j*42; |
|
const type = (Math.random()<0.15 ? 'metal' : Math.random()<0.4 ? 'glass' : 'wood'); |
|
const color = type==='metal' ? 0x90a4ae : type==='glass' ? 0x81d4fa : 0xffcc80; |
|
const rect = scene.add.rectangle(x,y,44,38,color).setStrokeStyle(2,0x333333); |
|
scene.matter.add.gameObject(rect, { shape: { type:'rectangle', width:44, height:38 }}); |
|
rect.isBlock = true; rect.blockType = type; rect.scored = false; |
|
blocksGroup.push(rect); |
|
} |
|
} |
|
} |
|
|
|
|
|
for (let k=0;k<Math.min(3, 1+lvl); k++) { |
|
const tx = baseX + 40 + k*70; |
|
const ty = baseY - (2+k%2)*42 - 20; |
|
const container = scene.add.container(tx, ty); |
|
const body = scene.add.circle(0,0,18,0xa5d6a7).setStrokeStyle(2,0x2e7d32); |
|
const smile = scene.add.text(-9,-12, 'π€', {fontSize:'26px'}); |
|
container.add([body, smile]); |
|
scene.matter.add.gameObject(container, { shape: { type: 'circle', radius: 18 }}); |
|
container.isTarget = true; container.destroyed = false; |
|
targetsGroup.push(container); |
|
|
|
scene.matter.world.on('collisionactive', (ev) => { |
|
ev.pairs.forEach(pair => { |
|
if ((pair.bodyA === container.body || pair.bodyB === container.body)) { |
|
|
|
const speed = pair.collision.penetration && Math.hypot(pair.collision.penetration.x, pair.collision.penetration.y) || 0; |
|
if (speed > 1.1 && !container.destroyed) { |
|
container.destroyed = true; |
|
|
|
explodeAt(scene, container.x, container.y); |
|
try { scene.matter.world.remove(container.body); } catch(e){} |
|
container.destroy(); |
|
score += 80; |
|
updateUi(); |
|
} |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
scene.cameras.main.setScroll(0,0); |
|
scene.cameras.main.stopFollow(); |
|
} |
|
|
|
function drawTrajectory(scene, x0, y0, vx, vy) { |
|
|
|
clearTrajectory(); |
|
|
|
const g = 1.0 * 60; |
|
const steps = 25; |
|
const dt = 1/12; |
|
for (let i=0;i<steps;i++){ |
|
const t = i*dt; |
|
const px = x0 + vx * 60 * t; |
|
const py = y0 + vy * 60 * t + 0.5 * g * t * t; |
|
const dot = scene.add.circle(px, py, 3, 0x0d47a1).setAlpha(0.6).setDepth(1); |
|
dottedLines.push(dot); |
|
} |
|
} |
|
|
|
function clearTrajectory() { |
|
dottedLines.forEach(d=>d.destroy()); |
|
dottedLines = []; |
|
} |
|
|
|
function explodeAt(scene, x, y) { |
|
|
|
for (let i=0;i<18;i++){ |
|
const p = scene.add.circle(x, y, 4, 0xff8a80); |
|
scene.tweens.add({ targets: p, x: x + Phaser.Math.Between(-120,120), y: y + Phaser.Math.Between(-120,120), alpha: 0, duration: 500 + Math.random()*400, onComplete: ()=>p.destroy() }); |
|
} |
|
} |
|
|
|
function updateUi() { |
|
document.getElementById('score').innerText = score; |
|
document.getElementById('ammo').innerText = ammo; |
|
document.getElementById('stars').innerText = stars; |
|
} |
|
|
|
function resetLevel(scene) { |
|
score = 0; ammo = 5; stars = 0; |
|
currentHuman = null; isDragging=false; megaLaunchActive=false; |
|
updateUi(); |
|
|
|
window.location.reload(); |
|
} |
|
|
|
function nextLevel(scene) { |
|
level += 1; ammo = 5 + Math.min(level, 3); |
|
score = score + 50; |
|
currentHuman = null; |
|
buildLevel(scene, level); |
|
spawnHuman(scene); |
|
updateUi(); |
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |