Angry Birds, one level, built by GPT-6 Luna
Hostedvia Codex CLIOpenRouter
Show the code the model wroteHide the code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#d3efff" />
<title>Featherweight — Woodland Siege</title>
<style>
:root {
color-scheme: light;
--ink: #263c42;
--cream: #fff9eb;
--orange: #f0783d;
--green: #4f8c61;
--glass: rgba(255, 251, 239, .82);
--line: rgba(49, 72, 70, .12);
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b8e3f3; }
body { font-family: ui-rounded, "Avenir Next", Avenir, "Segoe UI", sans-serif; color: var(--ink); user-select: none; -webkit-user-select: none; }
canvas { display: block; width: 100vw; height: 100vh; touch-action: none; cursor: crosshair; }
.hud { position: fixed; inset: 0; pointer-events: none; padding: 22px 28px; display: flex; flex-direction: column; justify-content: space-between; }
.topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
.brand, .score-card, .chapter, .bottom-card, .result-card { background: var(--glass); border: 1px solid rgba(255,255,255,.8); box-shadow: 0 10px 30px rgba(43, 77, 77, .12), inset 0 1px rgba(255,255,255,.9); backdrop-filter: blur(12px); }
.brand { padding: 12px 16px 13px; border-radius: 18px; min-width: 214px; }
.brandline { display: flex; align-items: center; gap: 9px; color: #608174; font-size: 10px; font-weight: 900; letter-spacing: .17em; text-transform: uppercase; }
.brandmark { display: grid; place-items: center; width: 24px; height: 24px; color: white; background: #ee7650; border-radius: 9px 9px 9px 3px; transform: rotate(-7deg); font-size: 14px; box-shadow: inset 0 -3px rgba(120,49,41,.16); }
h1 { font-size: 20px; line-height: 1; margin: 8px 0 0; letter-spacing: -.035em; font-weight: 950; color: #294148; }
.topright { display: flex; align-items: flex-start; gap: 10px; }
.score-card { display: flex; align-items: center; gap: 16px; border-radius: 17px; padding: 9px 15px; min-height: 56px; }
.stat { min-width: 52px; }
.stat-label { color: #78908b; text-transform: uppercase; letter-spacing: .13em; font-size: 9px; font-weight: 900; display: block; margin-bottom: 2px; }
.stat-value { font-size: 18px; font-weight: 950; line-height: 1; color: #314c50; }
.birds { display: flex; gap: 5px; align-items: center; height: 18px; }
.bird-dot { width: 15px; height: 15px; display: inline-block; border-radius: 50%; background: #e6644f; border: 2px solid #fff1d4; box-shadow: 0 2px 0 rgba(118,54,42,.25); position: relative; }
.bird-dot::after { content: ""; position: absolute; width: 5px; height: 4px; right: -2px; top: 2px; background: #f2b946; clip-path: polygon(0 0,100% 50%,0 100%); }
.bird-dot.used { background: #bbc5bd; box-shadow: none; opacity: .48; }
.chapter { color: #668177; border-radius: 999px; padding: 10px 14px; text-transform: uppercase; font-weight: 900; letter-spacing: .1em; font-size: 10px; white-space: nowrap; }
.icon-button { pointer-events: auto; display: grid; place-items: center; width: 44px; height: 44px; border: 1px solid rgba(255,255,255,.85); border-radius: 15px; background: var(--glass); color: #476464; cursor: pointer; box-shadow: 0 8px 22px rgba(43,77,77,.11); transition: transform .15s, background .15s; }
.icon-button:hover { transform: translateY(-2px); background: #fffdf4; }
.icon-button:active { transform: translateY(1px); }
.icon-button svg { width: 19px; height: 19px; }
.bottomrow { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; }
.bottom-card { border-radius: 15px; padding: 10px 14px; display: flex; gap: 10px; align-items: center; color: #607871; font-size: 11px; font-weight: 850; letter-spacing: .04em; }
.gesture { width: 22px; height: 22px; flex: 0 0 22px; border: 1.5px solid #9cb8a7; border-radius: 8px; display: grid; place-items: center; color: #618d70; font-size: 13px; font-weight: 900; transform: rotate(-18deg); }
.auto-pill { color: #557b69; }
.auto-dot { width: 7px; height: 7px; background: #ee9258; border-radius: 50%; box-shadow: 0 0 0 4px rgba(238,146,88,.14); }
.result { display: none; position: fixed; inset: 0; z-index: 4; align-items: center; justify-content: center; background: rgba(31,56,58,.17); backdrop-filter: blur(3px); pointer-events: auto; }
.result.show { display: flex; animation: appear .25s ease-out; }
@keyframes appear { from { opacity: 0; } to { opacity: 1; } }
.result-card { width: min(390px, calc(100vw - 36px)); text-align: center; border-radius: 25px; padding: 28px 30px 25px; background: rgba(255,251,239,.94); box-shadow: 0 24px 70px rgba(32,57,55,.22), inset 0 1px rgba(255,255,255,.95); }
.result-kicker { color: #719080; text-transform: uppercase; letter-spacing: .19em; font-size: 10px; font-weight: 950; }
.result h2 { margin: 8px 0 4px; font-size: 34px; letter-spacing: -.055em; line-height: 1; font-weight: 1000; color: #29464a; }
.result-copy { color: #708782; margin: 8px 0 13px; font-size: 13px; font-weight: 650; }
.stars { height: 42px; color: #eab64b; font-size: 31px; letter-spacing: 7px; text-shadow: 0 2px #fff0ba; }
.stars .dim { color: #cbd4c8; text-shadow: none; }
.final-score { color: #385459; font-size: 14px; font-weight: 950; margin-bottom: 18px; }
.restart { border: 0; border-radius: 13px; background: #e9724b; color: white; padding: 13px 22px; font: inherit; font-weight: 950; letter-spacing: .025em; cursor: pointer; box-shadow: inset 0 -3px rgba(121,53,39,.2), 0 7px 15px rgba(233,114,75,.2); transition: transform .15s, filter .15s; }
.restart:hover { transform: translateY(-2px); filter: brightness(1.04); }
@media (max-width: 700px) {
.hud { padding: 13px; }
.brand { min-width: 0; padding: 9px 11px 10px; border-radius: 15px; }
.brandline { font-size: 8px; gap: 6px; }
.brandmark { width: 20px; height: 20px; font-size: 11px; }
h1 { font-size: 16px; }
.topbar { gap: 8px; }
.topright { gap: 6px; align-items: flex-start; }
.score-card { padding: 8px 9px; min-height: 49px; gap: 9px; border-radius: 14px; }
.stat { min-width: 42px; }
.stat-label { font-size: 7px; letter-spacing: .1em; }
.stat-value { font-size: 15px; }
.bird-dot { width: 12px; height: 12px; }
.chapter { display: none; }
.icon-button { width: 39px; height: 39px; border-radius: 13px; }
.bottomrow { align-items: flex-end; }
.bottom-card { font-size: 9px; padding: 9px 10px; gap: 7px; }
.auto-pill { max-width: 145px; }
}
@media (max-width: 420px) { .brandline span:last-child { display: none; } .score-card { gap: 6px; } .stat { min-width: 37px; } }
</style>
</head>
<body>
<canvas id="game" aria-label="A slingshot game. Drag the red bird back and release to launch at the pig fort."></canvas>
<div class="hud">
<div class="topbar">
<div class="brand">
<div class="brandline"><span class="brandmark">✦</span><span>Featherweight arcade</span></div>
<h1>Woodland Siege</h1>
</div>
<div class="topright">
<div class="chapter">Chapter 01 · Hilltop fort</div>
<div class="score-card">
<div class="stat"><span class="stat-label">Score</span><span class="stat-value" id="score">00000</span></div>
<div class="stat"><span class="stat-label">Birds left</span><div class="birds" id="birds"><i class="bird-dot"></i><i class="bird-dot"></i><i class="bird-dot"></i></div></div>
</div>
<button class="icon-button" id="restartTop" aria-label="Restart level" title="Restart level">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7v5h-5"/><path d="M19 12a7.5 7.5 0 1 1-2.2-5.3L20 9"/></svg>
</button>
</div>
</div>
<div class="bottomrow">
<div class="bottom-card"><span class="gesture">↶</span><span>DRAG BACK TO AIM · RELEASE TO FLY</span></div>
<div class="bottom-card auto-pill" id="modePill"><span class="auto-dot"></span><span id="modeText">AUTO PILOT · GRAB THE SLINGSHOT</span></div>
</div>
</div>
<div class="result" id="result" role="dialog" aria-modal="true" aria-labelledby="resultTitle">
<div class="result-card">
<div class="result-kicker" id="resultKicker">The flock has landed</div>
<h2 id="resultTitle">Level cleared!</h2>
<p class="result-copy" id="resultCopy">A fine bit of flying.</p>
<div class="stars" id="stars">★★★</div>
<div class="final-score" id="finalScore">0 points</div>
<button class="restart" id="restartResult">Fly again</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const $score = document.getElementById('score');
const $birds = document.getElementById('birds');
const $modeText = document.getElementById('modeText');
const $modePill = document.getElementById('modePill');
const $result = document.getElementById('result');
const $resultTitle = document.getElementById('resultTitle');
const $resultKicker = document.getElementById('resultKicker');
const $resultCopy = document.getElementById('resultCopy');
const $stars = document.getElementById('stars');
const $finalScore = document.getElementById('finalScore');
const M = window.Matter;
if (!M) {
document.getElementById('modeText').textContent = 'PHYSICS COULD NOT LOAD · REFRESH TO TRY AGAIN';
return;
}
const { Engine, Bodies, Body, Composite, Events, Sleeping, Vector } = M;
const engine = Engine.create({ enableSleeping: true });
engine.gravity.y = 1;
engine.gravity.scale = 0.001;
engine.positionIterations = 8;
engine.velocityIterations = 6;
engine.constraintIterations = 2;
const STEP = 1000 / 60;
const GRAVITY_STEP = engine.gravity.y * engine.gravity.scale * STEP * STEP;
const TAU = Math.PI * 2;
let W = 0, H = 0, DPR = 1, S = 1, groundY = 0, worldWidth = 0;
let sling = null, readyBird = null, blocks = [], pigs = [], shots = [], particles = [], floats = [], debris = [];
let score = 0, birdsUsed = 0, cameraX = 0, autoMode = true, autoTimer = 1.3, autoAim = 0, dragging = false, pointerId = null;
let pointer = { x: 0, y: 0 }, gameResult = null, resultAt = 0, lastShotAt = 0, aimState = false, levelReady = false;
let lastFrame = 0, accumulator = 0, now = 0, autoCycle = 0, shake = 0, nextBirdWait = 0;
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const ease = (a, b, t) => a + (b - a) * (1 - Math.exp(-t));
const alivePigs = () => pigs.filter(p => !p.isPopped).length;
function resize() {
W = window.innerWidth; H = window.innerHeight;
DPR = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
S = clamp(Math.min(W / 1280, H / 800), .55, 1.2);
groundY = H * .835;
worldWidth = W * 1.55;
makeLevel();
}
function makeLevel() {
Composite.clear(engine.world, false);
engine.pairs = M.Pairs.create();
blocks = []; pigs = []; shots = []; particles = []; floats = []; debris = [];
score = 0; birdsUsed = 0; cameraX = 0; gameResult = null; aimState = false; dragging = false; pointerId = null;
autoTimer = autoMode ? 1.35 : Infinity; autoAim = 0; autoCycle = 0; nextBirdWait = 0; lastShotAt = 0; shake = 0;
$result.classList.remove('show');
const floor = Bodies.rectangle(worldWidth / 2, groundY + 80 * S, worldWidth + 500, 160 * S, { isStatic: true, friction: .88, label: 'ground' });
floor.gameKind = 'ground'; Composite.add(engine.world, floor);
sling = { x: W * .155, y: groundY - 132 * S, baseY: groundY };
const cx = Math.max(W * .735, sling.x + 420 * S);
buildFort(cx);
readyBird = makeBird(sling.x, sling.y);
updateHud();
levelReady = true;
}
function addRect(x, y, w, h, kind, angle = 0, options = {}) {
const stone = kind === 'stone';
const body = Bodies.rectangle(x, y, w, h, {
angle, density: stone ? .0046 : .00125, friction: stone ? .82 : .72,
frictionStatic: .95, frictionAir: .008, restitution: stone ? .035 : .075,
chamfer: undefined, sleepThreshold: 50,
label: kind
});
body.gameKind = kind; body.gameW = w; body.gameH = h;
body.materialName = kind; body.breakStrength = options.breakStrength || 9.8;
body.paintSeed = Math.random() * 1000;
blocks.push(body); Composite.add(engine.world, body); Sleeping.set(body, true);
return body;
}
function buildFort(cx) {
const u = S;
const col = (x,y,w,h,k,ang=0) => addRect(x,y,w*u,h*u,k,ang);
const g = groundY;
// A stout stone plinth and two timber pillars make a stable, quiet start.
col(cx - 109*u, g - 20*u, 84, 40, 'stone');
col(cx + 109*u, g - 20*u, 84, 40, 'stone');
col(cx - 108*u, g - 112*u, 25, 144, 'wood');
col(cx + 108*u, g - 112*u, 25, 144, 'wood');
col(cx, g - 190*u, 242, 22, 'wood');
// Two compact rooms with pigs on their floors.
col(cx - 82*u, g - 244*u, 22, 88, 'wood');
col(cx + 82*u, g - 244*u, 22, 88, 'wood');
col(cx, g - 200*u, 176, 18, 'wood');
col(cx, g - 291*u, 206, 23, 'stone');
col(cx - 99*u, g - 334*u, 21, 73, 'wood');
col(cx + 99*u, g - 334*u, 21, 73, 'wood');
col(cx, g - 376*u, 246, 22, 'wood');
// Corner braces and weighted roof stones give the collapse some variety.
col(cx - 132*u, g - 119*u, 18, 64, 'wood', -.23);
col(cx + 132*u, g - 119*u, 18, 64, 'wood', .23);
col(cx - 62*u, g - 352*u, 52, 37, 'stone');
col(cx + 62*u, g - 352*u, 52, 37, 'stone');
const pigSpec = [
[cx - 43*u, g - 222*u, 21*u], [cx + 43*u, g - 222*u, 21*u], [cx, g - 326*u, 20*u]
];
for (const [x,y,r] of pigSpec) {
const pig = Bodies.circle(x, y, r, { density: .0014, friction: .8, frictionAir: .012, restitution: .015, label: 'pig', sleepThreshold: 50 });
pig.gameKind = 'pig'; pig.gameR = r; pig.isPopped = false; pig.blinkSeed = Math.random() * 10;
pigs.push(pig); Composite.add(engine.world, pig); Sleeping.set(pig, true);
}
}
function makeBird(x, y) {
const r = 25 * S;
const body = Bodies.circle(x, y, r, { density: .0028, friction: .68, frictionAir: .0028, restitution: .31, label: 'bird', sleepThreshold: 45 });
body.gameKind = 'bird'; body.gameR = r; body.isReady = true; body.isLaunched = false; body.flightAge = 0; body.flightState = 'ready';
Composite.add(engine.world, body); Sleeping.set(body, true);
return body;
}
function bodyPosition(body) { return body ? { x: body.position.x, y: body.position.y } : sling; }
function aimPositionFromPointer(p) {
if (!readyBird) return;
let dx = p.x - sling.x, dy = p.y - sling.y;
const maxPull = 123 * S;
const mag = Math.hypot(dx, dy);
if (mag > maxPull) { dx *= maxPull / mag; dy *= maxPull / mag; }
dx = Math.min(dx, 13 * S);
Body.setPosition(readyBird, { x: sling.x + dx, y: sling.y + dy });
Body.setVelocity(readyBird, { x: 0, y: 0 }); Body.setAngularVelocity(readyBird, 0);
Sleeping.set(readyBird, true);
aimState = true;
}
function launch(body) {
if (!body || gameResult) return;
const dx = sling.x - body.position.x, dy = sling.y - body.position.y;
if (Math.hypot(dx, dy) < 9 * S) return;
const power = .205;
Body.setVelocity(body, { x: dx * power, y: dy * power });
Body.setAngularVelocity(body, rand(-.09, .09));
Sleeping.set(body, false);
body.isReady = false; body.isLaunched = true; body.flightState = 'flying'; body.flightAge = 0; body.lastEffectAt = 0;
shots.push(body); readyBird = null; birdsUsed++; lastShotAt = now; nextBirdWait = .62;
aimState = false; dragging = false; autoAim = 0; autoTimer = autoMode ? 1.05 : Infinity;
updateHud();
addFloat(body.position.x, body.position.y - 35*S, 'FLY!', '#fff4c8', 1.1);
addParticles(body.position.x, body.position.y, 'feather', 5, '#e96d4e');
}
function makeNextBird() {
if (readyBird || birdsUsed >= 3 || gameResult) return;
readyBird = makeBird(sling.x, sling.y); autoTimer = autoMode ? 1.05 : Infinity; autoAim = 0; aimState = false; updateHud();
}
function autoPull() {
const tweak = autoCycle % 3;
const backX = (100 + tweak * 4) * S;
const down = [42, 52, 36][tweak] * S;
aimPositionFromPointer({ x: sling.x - backX, y: sling.y + down });
}
Events.on(engine, 'collisionStart', event => {
for (const pair of event.pairs) {
const a = pair.bodyA, b = pair.bodyB;
const ka = a.gameKind, kb = b.gameKind;
if (!ka || !kb || ka === 'ground' || kb === 'ground') continue;
const rel = Vector.sub(a.velocity, b.velocity);
const impact = Vector.magnitude(rel);
const contact = pair.collision && pair.collision.supports && pair.collision.supports.length ? pair.collision.supports[0] : { x: (a.position.x+b.position.x)/2, y: (a.position.y+b.position.y)/2 };
if (ka === 'wood' && impact > a.breakStrength && (kb === 'bird' || impact > 12.1)) breakWood(a, contact, impact);
if (kb === 'wood' && impact > b.breakStrength && (ka === 'bird' || impact > 12.1)) breakWood(b, contact, impact);
checkPigImpact(a,b,impact,contact);
if ((ka === 'bird' || kb === 'bird') && impact > 3 && now - Math.max(a.lastEffectAt||0,b.lastEffectAt||0) > 360) {
const bird = ka === 'bird' ? a : b;
if (bird.isLaunched) {
bird.lastEffectAt = now;
shake = Math.min(8*S, impact*.25*S);
addParticles(contact.x, contact.y, impact > 9 ? 'dust' : 'spark', impact > 9 ? 9 : 5, impact > 9 ? '#e9c692' : '#fff0a6');
if (impact > 5) addFloat(contact.x, contact.y-14*S, `+${Math.round(impact*8)}`, '#fff5d5', .85);
}
}
}
});
Events.on(engine, 'collisionActive', event => {
for (const pair of event.pairs) {
const a=pair.bodyA,b=pair.bodyB;
if(a.gameKind!=='pig' && b.gameKind!=='pig') continue;
const impact=Vector.magnitude(Vector.sub(a.velocity,b.velocity));
const contact=pair.collision && pair.collision.supports && pair.collision.supports.length ? pair.collision.supports[0] : {x:(a.position.x+b.position.x)/2,y:(a.position.y+b.position.y)/2};
checkPigImpact(a,b,impact,contact);
}
});
function checkPigImpact(a,b,impact,contact) {
const pig=a.gameKind==='pig'?a:(b.gameKind==='pig'?b:null);
if(!pig || pig.isPopped) return;
const other=pig===a?b:a;
if(other.gameKind==='bird' && impact>4.7) { popPig(pig,contact); return; }
if(!['wood','stone','shard'].includes(other.gameKind)) return;
if(impact>8.1 || (other.gameKind==='stone' && impact>5.6)) { popPig(pig,contact); return; }
const above=other.position.y < pig.position.y - pig.gameR*.18;
if(above) {
pig.crushSince ??= now;
pig.lastCrushContact=now;
if(now-pig.crushSince>260) popPig(pig,contact);
}
}
function breakWood(body, at, impact) {
if (!body || body.isBroken) return;
body.isBroken = true;
const idx = blocks.indexOf(body); if (idx >= 0) blocks.splice(idx, 1);
Composite.remove(engine.world, body);
score += 45; updateHud();
addParticles(at.x, at.y, 'wood', 15, '#b97643');
addFloat(at.x, at.y-20*S, '+45', '#fff2c9', 1);
if (debris.length < 12) {
const count = impact > 15 ? 3 : 2;
for (let i=0;i<count;i++) {
const shard = Bodies.rectangle(at.x+rand(-8,8)*S, at.y+rand(-7,7)*S, rand(19,31)*S, rand(7,11)*S, { density:.0009, friction:.55, frictionAir:.018, restitution:.2, label:'shard' });
shard.gameKind='shard'; shard.gameW=26*S; shard.gameH=9*S; shard.paintSeed=Math.random()*1000; shard.ttl=2500;
Composite.add(engine.world, shard); Body.setVelocity(shard,{x:rand(-8,8),y:rand(-9,2)}); Body.setAngularVelocity(shard,rand(-.18,.18)); debris.push(shard);
}
}
}
function popPig(pig, at) {
if (!pig || pig.isPopped) return;
pig.isPopped = true; Composite.remove(engine.world, pig);
score += 500; updateHud();
addParticles(at.x, at.y, 'pig', 20, '#72bd60');
addParticles(at.x, at.y, 'feather', 7, '#f6d483');
addFloat(at.x, at.y-25*S, 'OINK! +500', '#fff2b9', 1.25);
shake = 13*S;
if (alivePigs() === 0) endGame(true);
}
function updateHud() {
$score.textContent = String(score).padStart(5, '0');
const remaining = Math.max(0, 3 - birdsUsed);
[...$birds.children].forEach((el, i) => el.classList.toggle('used', i >= remaining));
$modeText.textContent = autoMode ? 'AUTO PILOT · GRAB THE SLINGSHOT' : 'YOUR TURN · PULL BACK TO AIM';
$modePill.style.opacity = autoMode ? '1' : '.9';
}
function endGame(won) {
if (gameResult) return;
gameResult = won ? 'win' : 'lose'; resultAt = now;
$resultTitle.textContent = won ? 'Level cleared!' : 'Try again';
$resultKicker.textContent = won ? 'The fort has fallen' : 'The pigs are still in there';
$resultCopy.textContent = won ? 'A fine bit of flying.' : 'Take another run at that wobbly fort.';
const stars = won ? (birdsUsed <= 1 ? 3 : birdsUsed === 2 ? 2 : 1) : 0;
$stars.innerHTML = won ? `${'★'.repeat(stars)}<span class="dim">${'★'.repeat(3-stars)}</span>` : '<span class="dim">★★★</span>';
$finalScore.textContent = `${score.toLocaleString()} points · ${birdsUsed} bird${birdsUsed === 1 ? '' : 's'} used`;
$result.classList.add('show');
updateHud();
}
function addParticles(x,y,type,count,color) {
for(let i=0;i<count;i++) {
const feather = type==='feather';
particles.push({ x:x+rand(-8,8)*S,y:y+rand(-8,8)*S,vx:rand(-4,4)*S,vy:rand(-6,2)*S,life:rand(.45,1.05),max:1,size:(feather?rand(5,12):rand(2,7))*S,rot:rand(0,TAU),spin:rand(-.14,.14),type,color });
}
}
function addFloat(x,y,text,color='#fff5d7',life=1) { floats.push({x,y,text,color,life,max:life}); }
function roundedRect(c,x,y,w,h,r) {
r=Math.min(r,w/2,h/2); c.beginPath(); c.moveTo(x+r,y); c.arcTo(x+w,y,x+w,y+h,r); c.arcTo(x+w,y+h,x,y+h,r); c.arcTo(x,y+h,x,y,r); c.arcTo(x,y,x+w,y,r); c.closePath();
}
function hill(c, offset, y, amp, fill, phase) {
c.beginPath(); c.moveTo(-10,H+4); c.lineTo(-10,y);
for(let x=-10;x<=W+20;x+=W/7) c.quadraticCurveTo(x+W/14,y-amp*Math.sin((x+offset)/W*TAU+phase),x+W/7,y+amp*.3*Math.cos((x+offset)/W*TAU+phase));
c.lineTo(W+20,H+4); c.closePath(); c.fillStyle=fill; c.fill();
}
function drawBackground() {
const sky=ctx.createLinearGradient(0,0,0,H); sky.addColorStop(0,'#a9def6'); sky.addColorStop(.55,'#d6f0ee'); sky.addColorStop(1,'#fff0ce');
ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
const sunX=W*.79-cameraX*.12, sunY=H*.18, sr=48*S;
const halo=ctx.createRadialGradient(sunX,sunY,5,sunX,sunY,sr*2.3);halo.addColorStop(0,'rgba(255,244,192,.68)');halo.addColorStop(1,'rgba(255,244,192,0)');ctx.fillStyle=halo;ctx.fillRect(sunX-sr*2.3,sunY-sr*2.3,sr*4.6,sr*4.6);
ctx.beginPath();ctx.arc(sunX,sunY,sr*.52,0,TAU);ctx.fillStyle='#fff3c5';ctx.fill();
// Soft little cloud banks drift with the distant ridge.
for(let i=0;i<4;i++) {
const x=((i*W*.36-cameraX*.18+W*2)%(W*1.45))-W*.1, y=H*(.15+(i%3)*.085), q=16*S;
ctx.fillStyle='rgba(255,255,255,.36)';ctx.beginPath();ctx.ellipse(x,y,q*2.1,q*.55,0,0,TAU);ctx.ellipse(x-q*.65,y-q*.3,q*.76,q*.8,0,0,TAU);ctx.ellipse(x+q*.5,y-q*.2,q*.98,q*.92,0,0,TAU);ctx.fill();
}
hill(ctx,cameraX*.12,H*.62,H*.075,'#a9d6bd',.4);
hill(ctx,cameraX*.27,H*.70,H*.10,'#83c29d',1.7);
hill(ctx,cameraX*.42,H*.77,H*.07,'#65ae83',3.1);
// Distant pines are simple rounded silhouettes.
ctx.fillStyle='rgba(69,142,104,.42)';
for(let i=0;i<14;i++) {
const x=((i*W*.115-cameraX*.31+W*3)%(W*1.4))-W*.15, y=H*(.71+(i%4)*.025), ht=(32+(i%3)*10)*S;
ctx.beginPath();ctx.moveTo(x,y-ht);ctx.lineTo(x-ht*.36,y);ctx.lineTo(x+ht*.36,y);ctx.closePath();ctx.fill();
}
}
function drawGround() {
ctx.save();ctx.translate(-cameraX,0);
const x0=-cameraX-100, width=worldWidth+300;
ctx.fillStyle='#c9945c';ctx.fillRect(x0,groundY,width,H-groundY+100);
const soil=ctx.createLinearGradient(0,groundY,0,H);soil.addColorStop(0,'#c89761');soil.addColorStop(1,'#a66f48');ctx.fillStyle=soil;ctx.fillRect(x0,groundY,width,H-groundY+100);
ctx.beginPath();ctx.moveTo(x0,groundY+2*S);ctx.lineTo(x0,groundY-8*S);
for(let x=x0;x<x0+width;x+=44*S) ctx.quadraticCurveTo(x+17*S,groundY-13*S+Math.sin(x*.017)*2*S,x+44*S,groundY-6*S+Math.sin(x*.012)*2*S);
ctx.lineTo(x0+width,groundY+6*S);ctx.lineTo(x0,groundY+6*S);ctx.closePath();
const grass=ctx.createLinearGradient(0,groundY-10*S,0,groundY+8*S);grass.addColorStop(0,'#5aab75');grass.addColorStop(.45,'#70bd79');grass.addColorStop(1,'#499363');ctx.fillStyle=grass;ctx.fill();
// Subtle earth strata, pebbles and small grass tufts.
for(let i=0;i<24;i++) {
const x=((i*91*S+47*S)%(worldWidth+150))-50*S;
ctx.fillStyle=i%2?'rgba(120,75,48,.12)':'rgba(250,212,151,.23)';
ctx.beginPath();ctx.ellipse(x,groundY+(30+(i%5)*25)*S,rand(7,14)*S,rand(2,4)*S,0,0,TAU);ctx.fill();
}
for(let i=0;i<38;i++) {
const x=((i*67*S+19*S)%(worldWidth+100))-30*S, yy=groundY-7*S;
ctx.strokeStyle=i%2?'#478e61':'#79bf75';ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(x,yy+2*S);ctx.quadraticCurveTo(x-3*S,yy-5*S,x-4*S,yy-8*S);ctx.moveTo(x,yy+2*S);ctx.quadraticCurveTo(x+2*S,yy-5*S,x+5*S,yy-7*S);ctx.stroke();
}
ctx.restore();
}
function drawSling(back) {
const x=sling.x,y=sling.y, base=sling.baseY;
ctx.save();ctx.translate(-cameraX,0);
if(back) {
ctx.lineCap='round';ctx.lineJoin='round';
// Back fork and the elastic straps.
ctx.strokeStyle='#75402d';ctx.lineWidth=13*S;ctx.beginPath();ctx.moveTo(x+2*S,base+4*S);ctx.quadraticCurveTo(x-8*S,base-64*S,x-14*S,y-3*S);ctx.stroke();
ctx.strokeStyle='#b87743';ctx.lineWidth=8*S;ctx.beginPath();ctx.moveTo(x+1*S,base-2*S);ctx.quadraticCurveTo(x-7*S,base-73*S,x-14*S,y-3*S);ctx.stroke();
ctx.strokeStyle='#dcb37a';ctx.lineWidth=4*S;ctx.beginPath();ctx.moveTo(x-14*S,y-5*S);ctx.lineTo(x-8*S,y-5*S);ctx.moveTo(x-14*S,y-5*S);ctx.lineTo(x-8*S,y+3*S);ctx.stroke();
if(readyBird && (aimState || autoAim>0 || autoMode)) {
const p=bodyPosition(readyBird);ctx.strokeStyle='#654637';ctx.lineWidth=4*S;ctx.beginPath();ctx.moveTo(x-12*S,y-3*S);ctx.lineTo(p.x,p.y);ctx.moveTo(x-8*S,y+3*S);ctx.lineTo(p.x,p.y);ctx.stroke();
}
} else {
// Front fork crosses the pocket and gives the sling a hand-cut wooden look.
ctx.strokeStyle='#75402d';ctx.lineWidth=14*S;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(x-8*S,base+3*S);ctx.quadraticCurveTo(x+4*S,base-63*S,x+15*S,y-2*S);ctx.stroke();
ctx.strokeStyle='#c3834b';ctx.lineWidth=8*S;ctx.beginPath();ctx.moveTo(x-8*S,base-4*S);ctx.quadraticCurveTo(x+4*S,base-65*S,x+15*S,y-2*S);ctx.stroke();
ctx.strokeStyle='#e0b576';ctx.lineWidth=3*S;ctx.beginPath();ctx.moveTo(x+10*S,y-9*S);ctx.lineTo(x+16*S,y-1*S);ctx.moveTo(x+10*S,y-8*S);ctx.lineTo(x+4*S,y-5*S);ctx.stroke();
}
ctx.restore();
}
function drawTrajectory() {
if(!readyBird || gameResult || !(aimState || autoAim>0)) return;
const p=readyBird.position, vx=(sling.x-p.x)*.205, vy=(sling.y-p.y)*.205;
ctx.save();ctx.translate(-cameraX,0);
for(let i=1;i<=16;i++) {
const t=i*3.05, x=p.x+vx*t, y=p.y+vy*t+.5*GRAVITY_STEP*t*t;
if(y>groundY-3*S) break;
const r=(4.3-i*.16)*S, alpha=.76-i*.034;
ctx.beginPath();ctx.arc(x,y,Math.max(1.2*S,r),0,TAU);ctx.fillStyle=`rgba(255,247,218,${alpha})`;ctx.fill();
ctx.beginPath();ctx.arc(x,y,Math.max(.7*S,r*.38),0,TAU);ctx.fillStyle='rgba(237,126,77,.88)';ctx.fill();
}
ctx.restore();
}
function drawWood(body) {
const w=body.gameW,h=body.gameH,seed=body.paintSeed||0;
ctx.save();ctx.translate(body.position.x,body.position.y);ctx.rotate(body.angle);
// A soft offset shadow grounds each plank without making it look like UI art.
ctx.fillStyle='rgba(54,60,44,.16)';roundedRect(ctx,-w/2+2*S,-h/2+4*S,w,h,Math.min(5*S,h*.28));ctx.fill();
const g=ctx.createLinearGradient(0,-h/2,0,h/2);g.addColorStop(0,'#dca66a');g.addColorStop(.34,'#c78a51');g.addColorStop(1,'#aa6d3c');
roundedRect(ctx,-w/2,-h/2,w,h,Math.min(5*S,h*.28));ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#87552f';ctx.lineWidth=1.5*S;ctx.stroke();
ctx.save();roundedRect(ctx,-w/2+2*S,-h/2+2*S,w-4*S,h-4*S,Math.min(4*S,h*.26));ctx.clip();
const horizontal=w>=h;
for(let i=0;i<3;i++) {
ctx.beginPath();ctx.strokeStyle=i===1?'rgba(255,213,146,.45)':'rgba(107,61,32,.2)';ctx.lineWidth=(i===1?1.7:1)*S;
if(horizontal) { const yy=-h*.25+i*h*.25;ctx.moveTo(-w/2+5*S,yy);ctx.bezierCurveTo(-w*.2,yy+Math.sin(seed+i)*2*S,w*.18,yy-2*S,w/2-5*S,yy+Math.cos(seed+i)*2*S); }
else { const xx=-w*.25+i*w*.25;ctx.moveTo(xx,-h/2+4*S);ctx.bezierCurveTo(xx+Math.sin(seed+i)*2*S,-h*.2,xx-2*S,h*.2,xx+Math.cos(seed+i)*2*S,h/2-4*S); }
ctx.stroke();
}
for(const side of [-1,1]) { ctx.beginPath();ctx.arc(side*(w/2-5*S),0,1.6*S,0,TAU);ctx.fillStyle='#68482f';ctx.fill();ctx.beginPath();ctx.arc(side*(w/2-5*S)-.4*S,-.4*S,.55*S,0,TAU);ctx.fillStyle='#e6bd83';ctx.fill(); }
ctx.restore();ctx.restore();
}
function drawStone(body) {
const w=body.gameW,h=body.gameH;
ctx.save();ctx.translate(body.position.x,body.position.y);ctx.rotate(body.angle);
ctx.fillStyle='rgba(43,58,54,.15)';roundedRect(ctx,-w/2+2*S,-h/2+4*S,w,h,5*S);ctx.fill();
const g=ctx.createLinearGradient(0,-h/2,0,h/2);g.addColorStop(0,'#a8b5aa');g.addColorStop(.42,'#889b91');g.addColorStop(1,'#6f847d');
roundedRect(ctx,-w/2,-h/2,w,h,5*S);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#63786f';ctx.lineWidth=1.6*S;ctx.stroke();
ctx.strokeStyle='rgba(235,242,219,.5)';ctx.lineWidth=1.2*S;ctx.beginPath();ctx.moveTo(-w*.32,-h*.22);ctx.lineTo(w*.24,-h*.22);ctx.moveTo(-w*.38,-h*.12);ctx.lineTo(-w*.28,-h*.04);ctx.lineTo(-w*.34,h*.07);ctx.moveTo(w*.29,h*.05);ctx.lineTo(w*.19,h*.14);ctx.lineTo(w*.28,h*.23);ctx.stroke();
ctx.fillStyle='rgba(255,255,255,.24)';roundedRect(ctx,-w/2+4*S,-h/2+3*S,w-8*S,2*S,1*S);ctx.fill();
ctx.restore();
}
function drawPig(body) {
const r=body.gameR, x=body.position.x,y=body.position.y;
ctx.save();ctx.translate(x,y);ctx.rotate(body.angle*.25);
// ears
ctx.fillStyle='#62ae55';ctx.beginPath();ctx.ellipse(-r*.62,-r*.68,r*.34,r*.43,-.38,0,TAU);ctx.ellipse(r*.62,-r*.68,r*.34,r*.43,.38,0,TAU);ctx.fill();
ctx.fillStyle='#92d675';ctx.beginPath();ctx.ellipse(-r*.62,-r*.68,r*.17,r*.25,-.38,0,TAU);ctx.ellipse(r*.62,-r*.68,r*.17,r*.25,.38,0,TAU);ctx.fill();
const g=ctx.createRadialGradient(-r*.35,-r*.42,r*.08,0,0,r*1.25);g.addColorStop(0,'#9ade72');g.addColorStop(.62,'#71c35b');g.addColorStop(1,'#4a9b4e');ctx.beginPath();ctx.arc(0,0,r,0,TAU);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#418749';ctx.lineWidth=1.7*S;ctx.stroke();
// cheek blush
ctx.fillStyle='rgba(242,155,123,.38)';ctx.beginPath();ctx.ellipse(-r*.62,r*.2,r*.2,r*.12,0,0,TAU);ctx.ellipse(r*.62,r*.2,r*.2,r*.12,0,0,TAU);ctx.fill();
// eyes with tiny worried brows
for(const side of [-1,1]) {
const ex=side*r*.34, ey=-r*.18;
ctx.fillStyle='#fff7dc';ctx.beginPath();ctx.ellipse(ex,ey,r*.22,r*.28,side*.12,0,TAU);ctx.fill();
ctx.fillStyle='#344449';ctx.beginPath();ctx.arc(ex+side*r*.055,ey+r*.02,r*.095,0,TAU);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(ex+side*r*.075,ey-r*.015,r*.035,0,TAU);ctx.fill();
ctx.strokeStyle='#365842';ctx.lineWidth=2.5*S;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(ex-r*.17,ey-r*.35);ctx.lineTo(ex+r*.12,ey-r*.43);ctx.stroke();
}
// snout
const sg=ctx.createLinearGradient(0,r*.05,0,r*.65);sg.addColorStop(0,'#b4ea80');sg.addColorStop(1,'#80cc65');ctx.fillStyle=sg;ctx.beginPath();ctx.ellipse(0,r*.28,r*.44,r*.31,0,0,TAU);ctx.fill();ctx.strokeStyle='rgba(66,143,69,.5)';ctx.lineWidth=1.2*S;ctx.stroke();
ctx.fillStyle='#47894e';ctx.beginPath();ctx.ellipse(-r*.16,r*.29,r*.055,r*.105,-.1,0,TAU);ctx.ellipse(r*.16,r*.29,r*.055,r*.105,.1,0,TAU);ctx.fill();
ctx.restore();
}
function drawBird(body) {
const r=body.gameR,x=body.position.x,y=body.position.y;
ctx.save();ctx.translate(x,y);ctx.rotate(body.angle*.4);
// tail feathers
ctx.fillStyle='#c64a3e';ctx.beginPath();ctx.ellipse(-r*.84,-r*.07,r*.35,r*.16,-.55,0,TAU);ctx.ellipse(-r*.9,r*.17,r*.32,r*.14,-.14,0,TAU);ctx.fill();
const g=ctx.createRadialGradient(-r*.35,-r*.45,r*.04,0,0,r*1.35);g.addColorStop(0,'#fa8860');g.addColorStop(.52,'#e65d4b');g.addColorStop(1,'#bc3f3e');ctx.beginPath();ctx.arc(0,0,r,0,TAU);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#a8443c';ctx.lineWidth=1.7*S;ctx.stroke();
// crown tufts
ctx.fillStyle='#c44940';ctx.beginPath();ctx.ellipse(-r*.18,-r*.91,r*.14,r*.35,-.4,0,TAU);ctx.ellipse(r*.1,-r*.94,r*.13,r*.32,.35,0,TAU);ctx.fill();
for(const side of [-1,1]) {
const ex=side*r*.25, ey=-r*.2;
ctx.fillStyle='#fff8e8';ctx.beginPath();ctx.ellipse(ex,ey,r*.22,r*.28,0,0,TAU);ctx.fill();
ctx.fillStyle='#253e45';ctx.beginPath();ctx.arc(ex+r*.045,ey+r*.04,r*.09,0,TAU);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(ex+r*.066,ey+r*.005,r*.032,0,TAU);ctx.fill();
}
ctx.strokeStyle='#5c3433';ctx.lineWidth=4*S;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(-r*.51,-r*.5);ctx.lineTo(-r*.07,-r*.38);ctx.moveTo(r*.07,-r*.39);ctx.lineTo(r*.51,-r*.51);ctx.stroke();
const bg=ctx.createLinearGradient(0,-r*.02,0,r*.5);bg.addColorStop(0,'#ffd76a');bg.addColorStop(1,'#e99737');ctx.fillStyle=bg;ctx.beginPath();ctx.moveTo(r*.28,-r*.02);ctx.lineTo(r*1.13,r*.12);ctx.lineTo(r*.28,r*.31);ctx.closePath();ctx.fill();ctx.strokeStyle='#a9662d';ctx.lineWidth=1*S;ctx.stroke();
ctx.restore();
}
function drawBody(body) {
if(body.gameKind==='wood' || body.gameKind==='shard') drawWood(body);
else if(body.gameKind==='stone') drawStone(body);
else if(body.gameKind==='pig') drawPig(body);
else if(body.gameKind==='bird') drawBird(body);
}
function drawQueue() {
if(birdsUsed===0 || gameResult) return;
ctx.save();ctx.translate(-cameraX,0);
const n=Math.max(0,3-birdsUsed-(readyBird?1:0));
if(n>0) {
ctx.fillStyle='rgba(48,82,67,.54)';ctx.font=`900 ${9*S}px ui-rounded, sans-serif`;ctx.textAlign='left';ctx.fillText('ON DECK',sling.x+39*S,groundY-56*S);
for(let i=0;i<n;i++) {
const bx=sling.x+55*S+i*34*S, by=groundY-21*S, rr=12*S;
ctx.beginPath();ctx.arc(bx,by,rr,0,TAU);ctx.fillStyle='#df624c';ctx.fill();ctx.strokeStyle='#a8463e';ctx.lineWidth=1.2*S;ctx.stroke();
ctx.fillStyle='#fff4dd';ctx.beginPath();ctx.arc(bx-3*S,by-2*S,2.2*S,0,TAU);ctx.arc(bx+3*S,by-2*S,2.2*S,0,TAU);ctx.fill();ctx.fillStyle='#263b41';ctx.beginPath();ctx.arc(bx-2*S,by-2*S,1*S,0,TAU);ctx.arc(bx+4*S,by-2*S,1*S,0,TAU);ctx.fill();
}
}
ctx.restore();
}
function drawParticles(dt) {
for(let i=particles.length-1;i>=0;i--) {
const p=particles[i];p.life-=dt/1000;if(p.life<=0){particles.splice(i,1);continue;}
p.x+=p.vx*dt/16.67;p.y+=p.vy*dt/16.67;p.vy+=.22*S*dt/16.67;p.vx*=.992;p.rot+=p.spin*dt/16.67;
const alpha=clamp(p.life/p.max,0,1);
ctx.save();ctx.globalAlpha=alpha;ctx.translate(p.x,p.y);ctx.rotate(p.rot);
if(p.type==='feather') {ctx.fillStyle=p.color;ctx.beginPath();ctx.ellipse(0,0,p.size*.68,p.size*.2,0,0,TAU);ctx.fill();ctx.strokeStyle='rgba(255,255,255,.5)';ctx.lineWidth=.6*S;ctx.beginPath();ctx.moveTo(-p.size*.5,0);ctx.lineTo(p.size*.5,0);ctx.stroke();}
else if(p.type==='dust') {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*(1.1-alpha*.3),0,TAU);ctx.fill();}
else if(p.type==='pig') {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*(.7+alpha*.25),0,TAU);ctx.fill();}
else if(p.type==='wood') {ctx.fillStyle=p.color;roundedRect(ctx,-p.size*.8,-p.size*.24,p.size*1.6,p.size*.48,p.size*.12);ctx.fill();}
else {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*.52,0,TAU);ctx.fill();}
ctx.restore();
}
}
function drawFloats(dt) {
for(let i=floats.length-1;i>=0;i--) {
const f=floats[i];f.life-=dt/1000;f.y-=.44*S*dt/16.67;if(f.life<=0){floats.splice(i,1);continue;}
ctx.save();ctx.globalAlpha=clamp(f.life/f.max,0,1);ctx.translate(f.x,f.y);ctx.font=`1000 ${16*S}px ui-rounded, sans-serif`;ctx.textAlign='center';ctx.lineJoin='round';ctx.strokeStyle='rgba(63,66,48,.55)';ctx.lineWidth=3*S;ctx.strokeText(f.text,0,0);ctx.fillStyle=f.color;ctx.fillText(f.text,0,0);ctx.restore();
}
}
function draw(dt) {
ctx.setTransform(DPR,0,0,DPR,0,0);ctx.clearRect(0,0,W,H);
drawBackground();drawGround();
ctx.save();
if(shake>0) { ctx.translate(rand(-shake,shake),rand(-shake*.42,shake*.42)); shake*=.86; if(shake<.2)shake=0; }
drawSling(true);
ctx.save();drawTrajectory();ctx.translate(-cameraX,0);
const all=Composite.allBodies(engine.world).filter(b=>b.gameKind && b.gameKind!=='ground' && !b.isPopped);
all.sort((a,b)=>a.position.y-b.position.y);
for(const body of all) drawBody(body);
drawParticles(dt);drawFloats(dt);
ctx.restore();
drawSling(false);drawQueue();ctx.restore();
}
function step(dt) {
now += dt;
accumulator = Math.min(accumulator + dt, STEP*5);
while(accumulator >= STEP) { Engine.update(engine,STEP); accumulator-=STEP; }
const current = readyBird;
if(autoMode && !gameResult && current && !dragging) {
if(autoAim > 0) {
autoAim -= dt;
if(autoAim <= 0) launch(current);
} else {
autoTimer -= dt/1000;
if(autoTimer <= 0) { autoPull(); autoAim = .72; autoCycle++; }
}
}
for(const shot of shots) {
if(shot.flightState!=='flying') continue;
shot.flightAge += dt;
if(shot.position.y > groundY+145*S || shot.position.x > worldWidth+100 || shot.position.x < -100) {
Composite.remove(engine.world,shot);shot.flightState='settled';shot.wasRemoved=true;
} else if((shot.speed < .55 && shot.flightAge > 850) || shot.isSleeping || shot.flightAge > 4400) {
shot.flightState='settled';
if(shot.flightAge > 4400) { Sleeping.set(shot,true); Body.setVelocity(shot,{x:0,y:0}); }
nextBirdWait=.65;
}
}
for(const pig of pigs) if(!pig.isPopped && pig.position.y > groundY+22*S) popPig(pig,pig.position);
for(const pig of pigs) if(!pig.isPopped && pig.crushSince && now-(pig.lastCrushContact||0)>130) pig.crushSince=0;
for(let i=debris.length-1;i>=0;i--) {
debris[i].ttl-=dt;if(debris[i].ttl<=0 || debris[i].position.y>groundY+100*S) { Composite.remove(engine.world,debris[i]);debris.splice(i,1); }
}
if(!gameResult && !readyBird && birdsUsed<3 && shots.every(s=>s.flightState==='settled' || s.wasRemoved)) {
nextBirdWait-=dt/1000;
if(nextBirdWait<=0) makeNextBird();
}
if(!gameResult && birdsUsed>=3 && shots.every(s=>s.flightState==='settled' || s.wasRemoved) && now-lastShotAt>1900) endGame(false);
const liveShot=shots.find(s=>s.flightState==='flying');
const maxCam=Math.max(0, Math.min(W*.22, (sling.x+W*.82)-W*.6));
const targetCam=liveShot ? clamp(liveShot.position.x-W*.5,0,maxCam) : 0;
cameraX=ease(cameraX,targetCam,.055);
if(gameResult && autoMode && now-resultAt>2600) { autoCycle=0; makeLevel(); }
}
function frame(t) {
if(!lastFrame)lastFrame=t;const dt=Math.min(45,t-lastFrame);lastFrame=t;
if(levelReady)step(dt);draw(dt);requestAnimationFrame(frame);
}
function localPoint(ev) {
const rect=canvas.getBoundingClientRect();return { x:(ev.clientX-rect.left)*W/rect.width+cameraX, y:(ev.clientY-rect.top)*H/rect.height };
}
canvas.addEventListener('pointerdown', ev => {
if(gameResult) { if(autoMode) { autoMode=false;updateHud(); } return; }
const p=localPoint(ev);pointer=p;
const bird=readyBird;
const onBird=bird && Math.hypot(p.x-bird.position.x,p.y-bird.position.y) <= bird.gameR*1.8;
const nearSling=Math.hypot(p.x-sling.x,p.y-sling.y)<126*S;
if(!onBird && !nearSling) return;
autoMode=false;autoAim=0;autoTimer=Infinity;updateHud();
if(!onBird || !bird) return;
dragging=true;pointerId=ev.pointerId;canvas.setPointerCapture(pointerId);aimPositionFromPointer(p);
canvas.style.cursor='grabbing';ev.preventDefault();
});
canvas.addEventListener('pointermove', ev => {
if(!dragging || ev.pointerId!==pointerId)return;
pointer=localPoint(ev);aimPositionFromPointer(pointer);ev.preventDefault();
});
function releasePointer(ev) {
if(!dragging || (ev && ev.pointerId!==pointerId))return;
if(readyBird) launch(readyBird);
dragging=false;pointerId=null;canvas.style.cursor='crosshair';
}
canvas.addEventListener('pointerup',releasePointer);
canvas.addEventListener('pointercancel',releasePointer);
canvas.addEventListener('lostpointercapture', () => { if(dragging)releasePointer(); });
function restart(manual=true) { autoMode=!manual; makeLevel(); updateHud(); }
document.getElementById('restartTop').addEventListener('click',()=>restart(true));
document.getElementById('restartResult').addEventListener('click',()=>restart(true));
window.addEventListener('resize',resize);
resize();requestAnimationFrame(frame);
})();
</script>
</body>
</html>Same prompt, other models
One email when a new model lands on the board or in the showcase. Unsubscribe anytime.