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