|
|
@@ -69,18 +69,33 @@ const id = `kb-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
<circle cx="364" cy="130" r="18" fill="none" stroke="#2a4a75" stroke-width="1"/>
|
|
|
<circle cx="366" cy="132" r="5" fill="#000" opacity="0.4"/>
|
|
|
|
|
|
- <!-- 3 biscuit magnets — vertical row, center -->
|
|
|
- <circle cx="200" cy="88" r="11" fill="#e8e6e0"/><circle cx="200" cy="88" r="8" fill="#d0cdc6"/><circle cx="200" cy="88" r="5" fill="#b8b5ae"/><circle cx="198" cy="86" r="2" fill="#fff" opacity="0.5"/>
|
|
|
- <circle cx="200" cy="130" r="11" fill="#e8e6e0"/><circle cx="200" cy="130" r="8" fill="#d0cdc6"/><circle cx="200" cy="130" r="5" fill="#b8b5ae"/><circle cx="198" cy="128" r="2" fill="#fff" opacity="0.5"/>
|
|
|
- <circle cx="200" cy="172" r="11" fill="#e8e6e0"/><circle cx="200" cy="172" r="8" fill="#d0cdc6"/><circle cx="200" cy="172" r="5" fill="#b8b5ae"/><circle cx="198" cy="170" r="2" fill="#fff" opacity="0.5"/>
|
|
|
+ <!-- 3 biscuit magnets (animated, smaller — r=6) -->
|
|
|
+ <g id={`${id}-b0`}>
|
|
|
+ <circle r="6" fill="#e8e6e0"/>
|
|
|
+ <circle r="4.5" fill="#d0cdc6"/>
|
|
|
+ <circle r="2.5" fill="#b8b5ae"/>
|
|
|
+ <circle cx="-1" cy="-1" r="1.5" fill="#fff" opacity="0.5"/>
|
|
|
+ </g>
|
|
|
+ <g id={`${id}-b1`}>
|
|
|
+ <circle r="6" fill="#e8e6e0"/>
|
|
|
+ <circle r="4.5" fill="#d0cdc6"/>
|
|
|
+ <circle r="2.5" fill="#b8b5ae"/>
|
|
|
+ <circle cx="-1" cy="-1" r="1.5" fill="#fff" opacity="0.5"/>
|
|
|
+ </g>
|
|
|
+ <g id={`${id}-b2`}>
|
|
|
+ <circle r="6" fill="#e8e6e0"/>
|
|
|
+ <circle r="4.5" fill="#d0cdc6"/>
|
|
|
+ <circle r="2.5" fill="#b8b5ae"/>
|
|
|
+ <circle cx="-1" cy="-1" r="1.5" fill="#fff" opacity="0.5"/>
|
|
|
+ </g>
|
|
|
|
|
|
- <!-- Ball (animated) — yellow -->
|
|
|
+ <!-- Ball (animated, bigger — r=8) -->
|
|
|
<g id={`${id}-ball`}>
|
|
|
- <circle r="6" fill={`url(#ballglow-${id})`}/>
|
|
|
- <circle r="6" fill="none" stroke="#c8960a" stroke-width="0.8" opacity="0.6"/>
|
|
|
+ <circle r="8" fill={`url(#ballglow-${id})`}/>
|
|
|
+ <circle r="8" fill="none" stroke="#c8960a" stroke-width="0.8" opacity="0.6"/>
|
|
|
</g>
|
|
|
|
|
|
- <!-- Player 1 puck (animated) — left half, thinner: r=11 -->
|
|
|
+ <!-- Player 1 puck (animated) — r=11 -->
|
|
|
<g id={`${id}-p1`}>
|
|
|
<circle r="11" fill="#111"/>
|
|
|
<circle r="9" fill="#1c1c1c"/>
|
|
|
@@ -89,7 +104,7 @@ const id = `kb-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
<circle r="11" fill="none" stroke="#383838" stroke-width="1.2"/>
|
|
|
</g>
|
|
|
|
|
|
- <!-- Player 2 puck (animated) — right half, thinner: r=11 -->
|
|
|
+ <!-- Player 2 puck (animated) — r=11 -->
|
|
|
<g id={`${id}-p2`}>
|
|
|
<circle r="11" fill="#111"/>
|
|
|
<circle r="9" fill="#1c1c1c"/>
|
|
|
@@ -110,163 +125,336 @@ const id = `kb-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
const ballEl = document.getElementById(`${id}-ball`);
|
|
|
const p1El = document.getElementById(`${id}-p1`);
|
|
|
const p2El = document.getElementById(`${id}-p2`);
|
|
|
+ const bEls = [0,1,2].map(i => document.getElementById(`${id}-b${i}`));
|
|
|
|
|
|
- // Field bounds (playable area, inset from walls)
|
|
|
+ // Field bounds
|
|
|
const FX1 = 24, FX2 = 376, FY1 = 24, FY2 = 236;
|
|
|
- const CX = 200; // center divider x
|
|
|
-
|
|
|
- // Static objects
|
|
|
- const GOAL_L = { x: 36, y: 130, r: 22 };
|
|
|
- const GOAL_R = { x: 364, y: 130, r: 22 };
|
|
|
- const BISCUITS = [
|
|
|
- { x: 200, y: 88 },
|
|
|
- { x: 200, y: 130 },
|
|
|
- { x: 200, y: 172 },
|
|
|
- ];
|
|
|
- const BISCUIT_R = 11;
|
|
|
- const PUCK_R = 11;
|
|
|
- const BALL_R = 6;
|
|
|
+ const CX = 200;
|
|
|
+
|
|
|
+ // Goals
|
|
|
+ const GOAL_L = { x: 36, y: 130, r: 22 };
|
|
|
+ const GOAL_R = { x: 364, y: 130, r: 22 };
|
|
|
+
|
|
|
+ // Radii
|
|
|
+ const BALL_R = 8;
|
|
|
+ const BISC_R = 6;
|
|
|
+ const PUCK_R = 11;
|
|
|
|
|
|
// --- State ---
|
|
|
- let ball = { x: 200, y: 130, vx: 3.2, vy: 2.1 };
|
|
|
+ const ball = { x: 200, y: 130, vx: 0, vy: 0 };
|
|
|
|
|
|
- // Pucks wander in their half
|
|
|
- let p1 = { x: 95, y: 130, vx: 0, vy: 0, tx: 95, ty: 130, timer: 0 };
|
|
|
- let p2 = { x: 305, y: 130, vx: 0, vy: 0, tx: 305, ty: 130, timer: 0 };
|
|
|
+ // Each biscuit tracks which puck it's stuck to (null = free, 1 or 2)
|
|
|
+ const biscs = [
|
|
|
+ { x: 200, y: 88, vx: 0, vy: 0, stuck: null, angle: 0 },
|
|
|
+ { x: 200, y: 130, vx: 0, vy: 0, stuck: null, angle: 0 },
|
|
|
+ { x: 200, y: 172, vx: 0, vy: 0, stuck: null, angle: 0 },
|
|
|
+ ];
|
|
|
|
|
|
- function randTarget(side) {
|
|
|
- const margin = PUCK_R + 16;
|
|
|
- if (side === 1) {
|
|
|
- return {
|
|
|
- tx: FX1 + margin + Math.random() * (CX - FX1 - margin * 2),
|
|
|
- ty: FY1 + margin + Math.random() * (FY2 - FY1 - margin * 2),
|
|
|
- };
|
|
|
- } else {
|
|
|
- return {
|
|
|
- tx: CX + margin + Math.random() * (FX2 - CX - margin * 2),
|
|
|
- ty: FY1 + margin + Math.random() * (FY2 - FY1 - margin * 2),
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
+ const p1 = { x: 95, y: 130, vx: 0, vy: 0, attached: 0 };
|
|
|
+ const p2 = { x: 305, y: 130, vx: 0, vy: 0, attached: 0 };
|
|
|
+
|
|
|
+ const ATTACH_DIST = PUCK_R + BISC_R + 2; // snap distance
|
|
|
+ const STUCK_ORBIT = PUCK_R + BISC_R - 2; // orbit radius once stuck
|
|
|
|
|
|
- function dist(ax, ay, bx, by) {
|
|
|
+ function d(ax, ay, bx, by) {
|
|
|
const dx = ax - bx, dy = ay - by;
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
|
}
|
|
|
|
|
|
- function resolveCircleCollision(ball, bx, by, br) {
|
|
|
- const d = dist(ball.x, ball.y, bx, by);
|
|
|
- const minD = BALL_R + br;
|
|
|
- if (d < minD && d > 0) {
|
|
|
- const nx = (ball.x - bx) / d;
|
|
|
- const ny = (ball.y - by) / d;
|
|
|
- const dot = ball.vx * nx + ball.vy * ny;
|
|
|
- ball.vx -= 2 * dot * nx;
|
|
|
- ball.vy -= 2 * dot * ny;
|
|
|
- // Push ball out of overlap
|
|
|
- const overlap = minD - d;
|
|
|
- ball.x += nx * overlap;
|
|
|
- ball.y += ny * overlap;
|
|
|
- // Slight speed boost on hit so it doesn't die
|
|
|
- const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
|
|
|
- if (speed < 2) { ball.vx *= 2.5 / speed; ball.vy *= 2.5 / speed; }
|
|
|
- }
|
|
|
- }
|
|
|
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
|
|
|
|
- function clampSpeed(obj, max) {
|
|
|
- const s = Math.sqrt(obj.vx * obj.vx + obj.vy * obj.vy);
|
|
|
+ function speed(obj) { return Math.sqrt(obj.vx * obj.vx + obj.vy * obj.vy); }
|
|
|
+
|
|
|
+ function capSpeed(obj, max) {
|
|
|
+ const s = speed(obj);
|
|
|
if (s > max) { obj.vx = (obj.vx / s) * max; obj.vy = (obj.vy / s) * max; }
|
|
|
}
|
|
|
|
|
|
- function updatePuck(p, side, dt) {
|
|
|
- p.timer -= dt;
|
|
|
- if (p.timer <= 0) {
|
|
|
- const t = randTarget(side);
|
|
|
- p.tx = t.tx; p.ty = t.ty;
|
|
|
- p.timer = 1200 + Math.random() * 1800; // ms
|
|
|
+ // Elastic circle-circle collision between a (moving) and b (may be fixed or moving)
|
|
|
+ function collide(a, ar, b, br, bMass) {
|
|
|
+ const dist = d(a.x, a.y, b.x, b.y);
|
|
|
+ const minD = ar + br;
|
|
|
+ if (dist >= minD || dist < 0.01) return;
|
|
|
+
|
|
|
+ const nx = (a.x - b.x) / dist;
|
|
|
+ const ny = (a.y - b.y) / dist;
|
|
|
+
|
|
|
+ // Relative velocity
|
|
|
+ const dvx = a.vx - b.vx;
|
|
|
+ const dvy = a.vy - b.vy;
|
|
|
+ const dot = dvx * nx + dvy * ny;
|
|
|
+ if (dot > 0) return; // already separating
|
|
|
+
|
|
|
+ // Mass-weighted impulse (pucks are "infinite" mass vs ball/biscuits)
|
|
|
+ const totalMass = 1 + bMass;
|
|
|
+ const impulseA = (2 * bMass / totalMass) * dot;
|
|
|
+ const impulseB = (2 * 1 / totalMass) * dot;
|
|
|
+
|
|
|
+ a.vx -= impulseA * nx;
|
|
|
+ a.vy -= impulseA * ny;
|
|
|
+ b.vx += impulseB * nx;
|
|
|
+ b.vy += impulseB * ny;
|
|
|
+
|
|
|
+ // Push apart
|
|
|
+ const overlap = minD - dist + 0.5;
|
|
|
+ const pushA = bMass / totalMass;
|
|
|
+ const pushB = 1 / totalMass;
|
|
|
+ a.x += nx * overlap * pushA;
|
|
|
+ a.y += ny * overlap * pushA;
|
|
|
+ b.x -= nx * overlap * pushB;
|
|
|
+ b.y -= ny * overlap * pushB;
|
|
|
+ }
|
|
|
+
|
|
|
+ function wallBounce(obj, r) {
|
|
|
+ if (obj.x - r < FX1) { obj.x = FX1 + r; obj.vx = Math.abs(obj.vx) * 0.85; }
|
|
|
+ if (obj.x + r > FX2) { obj.x = FX2 - r; obj.vx = -Math.abs(obj.vx) * 0.85; }
|
|
|
+ if (obj.y - r < FY1) { obj.y = FY1 + r; obj.vy = Math.abs(obj.vy) * 0.85; }
|
|
|
+ if (obj.y + r > FY2) { obj.y = FY2 - r; obj.vy = -Math.abs(obj.vy) * 0.85; }
|
|
|
+ }
|
|
|
+
|
|
|
+ function resetBall() {
|
|
|
+ ball.x = 200; ball.y = 130;
|
|
|
+ const angle = Math.random() * Math.PI * 2;
|
|
|
+ const s = 2.5 + Math.random() * 1.5;
|
|
|
+ ball.vx = Math.cos(angle) * s;
|
|
|
+ ball.vy = Math.sin(angle) * s;
|
|
|
+ }
|
|
|
+
|
|
|
+ function resetBiscuits() {
|
|
|
+ biscs[0].x = 200; biscs[0].y = 88;
|
|
|
+ biscs[1].x = 200; biscs[1].y = 130;
|
|
|
+ biscs[2].x = 200; biscs[2].y = 172;
|
|
|
+ biscs.forEach(b => { b.vx = 0; b.vy = 0; b.stuck = null; b.angle = 0; });
|
|
|
+ p1.attached = 0;
|
|
|
+ p2.attached = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ function resetRound() {
|
|
|
+ resetBall();
|
|
|
+ resetBiscuits();
|
|
|
+ p1.x = 95; p1.y = 130; p1.vx = 0; p1.vy = 0;
|
|
|
+ p2.x = 305; p2.y = 130; p2.vx = 0; p2.vy = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Puck AI ---
|
|
|
+ // Each puck: chase ball when it's on their half, defend when it's not, always avoid own goal
|
|
|
+ function updatePuck(p, side) {
|
|
|
+ const myGoal = side === 1 ? GOAL_L : GOAL_R;
|
|
|
+ const oppGoal = side === 1 ? GOAL_R : GOAL_L;
|
|
|
+ const onMyHalf = side === 1 ? (ball.x < CX + 30) : (ball.x > CX - 30);
|
|
|
+
|
|
|
+ let tx, ty;
|
|
|
+
|
|
|
+ if (onMyHalf) {
|
|
|
+ // Intercept: predict where ball will be in ~15 frames
|
|
|
+ const look = 12 + Math.random() * 8;
|
|
|
+ tx = ball.x + ball.vx * look;
|
|
|
+ ty = ball.y + ball.vy * look;
|
|
|
+
|
|
|
+ // If we're close enough to the ball, try to aim it toward opponent goal
|
|
|
+ const distToBall = d(p.x, p.y, ball.x, ball.y);
|
|
|
+ if (distToBall < 50) {
|
|
|
+ // Position behind ball relative to opponent goal direction
|
|
|
+ const toGoalX = oppGoal.x - ball.x;
|
|
|
+ const toGoalY = oppGoal.y - ball.y;
|
|
|
+ const toGoalD = Math.sqrt(toGoalX * toGoalX + toGoalY * toGoalY) || 1;
|
|
|
+ tx = ball.x - (toGoalX / toGoalD) * 20;
|
|
|
+ ty = ball.y - (toGoalY / toGoalD) * 20;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Defensive: sit between goal and center, slightly tracking ball's Y
|
|
|
+ const defX = side === 1 ? 80 : 320;
|
|
|
+ tx = defX;
|
|
|
+ ty = 130 + (ball.y - 130) * 0.3;
|
|
|
}
|
|
|
- // Steer toward target
|
|
|
- const dx = p.tx - p.x, dy = p.ty - p.y;
|
|
|
- const d = Math.sqrt(dx * dx + dy * dy);
|
|
|
- if (d > 2) {
|
|
|
- p.vx += (dx / d) * 0.18;
|
|
|
- p.vy += (dy / d) * 0.18;
|
|
|
+
|
|
|
+ // Clamp target to own half
|
|
|
+ const margin = PUCK_R + 4;
|
|
|
+ if (side === 1) {
|
|
|
+ tx = clamp(tx, FX1 + margin, CX - margin);
|
|
|
+ } else {
|
|
|
+ tx = clamp(tx, CX + margin, FX2 - margin);
|
|
|
}
|
|
|
+ ty = clamp(ty, FY1 + margin, FY2 - margin);
|
|
|
+
|
|
|
+ // Avoid own goal hole — strong repulsion
|
|
|
+ const dGoal = d(tx, ty, myGoal.x, myGoal.y);
|
|
|
+ if (dGoal < PUCK_R + myGoal.r + 10) {
|
|
|
+ const nx = (tx - myGoal.x) / (dGoal || 1);
|
|
|
+ const ny = (ty - myGoal.y) / (dGoal || 1);
|
|
|
+ const pushOut = PUCK_R + myGoal.r + 14;
|
|
|
+ tx = myGoal.x + nx * pushOut;
|
|
|
+ ty = myGoal.y + ny * pushOut;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Steer toward target
|
|
|
+ const dx = tx - p.x, dy = ty - p.y;
|
|
|
+ const dd = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
|
+ const accel = onMyHalf ? 0.3 : 0.15;
|
|
|
+ p.vx += (dx / dd) * accel;
|
|
|
+ p.vy += (dy / dd) * accel;
|
|
|
+
|
|
|
// Damping
|
|
|
- p.vx *= 0.88;
|
|
|
- p.vy *= 0.88;
|
|
|
- clampSpeed(p, 3.5);
|
|
|
+ p.vx *= 0.85;
|
|
|
+ p.vy *= 0.85;
|
|
|
+ capSpeed(p, onMyHalf ? 4 : 2.5);
|
|
|
+
|
|
|
p.x += p.vx;
|
|
|
p.y += p.vy;
|
|
|
|
|
|
- // Clamp puck to its half
|
|
|
- const margin = PUCK_R + 2;
|
|
|
+ // Hard-clamp to own half
|
|
|
if (side === 1) {
|
|
|
- p.x = Math.max(FX1 + margin, Math.min(CX - margin, p.x));
|
|
|
+ p.x = clamp(p.x, FX1 + margin, CX - margin);
|
|
|
} else {
|
|
|
- p.x = Math.max(CX + margin, Math.min(FX2 - margin, p.x));
|
|
|
+ p.x = clamp(p.x, CX + margin, FX2 - margin);
|
|
|
}
|
|
|
- p.y = Math.max(FY1 + margin, Math.min(FY2 - margin, p.y));
|
|
|
+ p.y = clamp(p.y, FY1 + margin, FY2 - margin);
|
|
|
}
|
|
|
|
|
|
- function resetBall() {
|
|
|
- ball.x = 200; ball.y = 130;
|
|
|
- const angle = Math.random() * Math.PI * 2;
|
|
|
- const speed = 2.8 + Math.random();
|
|
|
- ball.vx = Math.cos(angle) * speed;
|
|
|
- ball.vy = Math.sin(angle) * speed;
|
|
|
- }
|
|
|
+ resetBall();
|
|
|
+
|
|
|
+ // Stuck detection: track ball position over time
|
|
|
+ let stuckX = ball.x, stuckY = ball.y, stuckTimer = 0;
|
|
|
+ const STUCK_RADIUS = 15; // if ball stays within this many px
|
|
|
+ const STUCK_TIMEOUT = 3000; // for this many ms, reset
|
|
|
|
|
|
let last = performance.now();
|
|
|
|
|
|
function tick(now) {
|
|
|
- const dt = Math.min(now - last, 32); // cap at 32ms
|
|
|
+ const dt = Math.min(now - last, 32);
|
|
|
last = now;
|
|
|
|
|
|
- // Update pucks
|
|
|
- updatePuck(p1, 1, dt);
|
|
|
- updatePuck(p2, 2, dt);
|
|
|
+ // Update puck AI
|
|
|
+ updatePuck(p1, 1);
|
|
|
+ updatePuck(p2, 2);
|
|
|
|
|
|
// Move ball
|
|
|
ball.x += ball.vx;
|
|
|
ball.y += ball.vy;
|
|
|
|
|
|
- // Wall collisions
|
|
|
- if (ball.x - BALL_R < FX1) { ball.x = FX1 + BALL_R; ball.vx = Math.abs(ball.vx); }
|
|
|
- if (ball.x + BALL_R > FX2) { ball.x = FX2 - BALL_R; ball.vx = -Math.abs(ball.vx); }
|
|
|
- if (ball.y - BALL_R < FY1) { ball.y = FY1 + BALL_R; ball.vy = Math.abs(ball.vy); }
|
|
|
- if (ball.y + BALL_R > FY2) { ball.y = FY2 - BALL_R; ball.vy = -Math.abs(ball.vy); }
|
|
|
+ // Stuck detection: if ball hasn't moved far in a while, reset round
|
|
|
+ if (d(ball.x, ball.y, stuckX, stuckY) < STUCK_RADIUS) {
|
|
|
+ stuckTimer += dt;
|
|
|
+ if (stuckTimer > STUCK_TIMEOUT) {
|
|
|
+ resetRound();
|
|
|
+ stuckTimer = 0;
|
|
|
+ stuckX = ball.x; stuckY = ball.y;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ stuckX = ball.x; stuckY = ball.y;
|
|
|
+ stuckTimer = 0;
|
|
|
+ }
|
|
|
|
|
|
- // Ball vs pucks
|
|
|
- resolveCircleCollision(ball, p1.x, p1.y, PUCK_R);
|
|
|
- resolveCircleCollision(ball, p2.x, p2.y, PUCK_R);
|
|
|
+ // Tiny friction
|
|
|
+ ball.vx *= 0.999;
|
|
|
+ ball.vy *= 0.999;
|
|
|
+ // Minimum speed so it stays lively
|
|
|
+ if (speed(ball) < 1.5) {
|
|
|
+ const s = speed(ball) || 1;
|
|
|
+ ball.vx = (ball.vx / s) * 1.5;
|
|
|
+ ball.vy = (ball.vy / s) * 1.5;
|
|
|
+ }
|
|
|
|
|
|
- // Ball vs biscuits (fixed)
|
|
|
- for (const b of BISCUITS) {
|
|
|
- resolveCircleCollision(ball, b.x, b.y, BISCUIT_R);
|
|
|
+ // --- Biscuit logic: free ones move, stuck ones orbit their puck ---
|
|
|
+ for (const b of biscs) {
|
|
|
+ if (b.stuck !== null) {
|
|
|
+ // Orbit: rotate slowly around the puck it's stuck to
|
|
|
+ const puck = b.stuck === 1 ? p1 : p2;
|
|
|
+ b.angle += 0.02;
|
|
|
+ b.x = puck.x + Math.cos(b.angle) * STUCK_ORBIT;
|
|
|
+ b.y = puck.y + Math.sin(b.angle) * STUCK_ORBIT;
|
|
|
+ b.vx = 0; b.vy = 0;
|
|
|
+ } else {
|
|
|
+ b.x += b.vx;
|
|
|
+ b.y += b.vy;
|
|
|
+ b.vx *= 0.97;
|
|
|
+ b.vy *= 0.97;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // Ball into goal → reset
|
|
|
- if (dist(ball.x, ball.y, GOAL_L.x, GOAL_L.y) < GOAL_L.r - 2 ||
|
|
|
- dist(ball.x, ball.y, GOAL_R.x, GOAL_R.y) < GOAL_R.r - 2) {
|
|
|
- resetBall();
|
|
|
+ // Wall bounces
|
|
|
+ wallBounce(ball, BALL_R);
|
|
|
+ for (const b of biscs) { if (b.stuck === null) wallBounce(b, BISC_R); }
|
|
|
+
|
|
|
+ // Ball vs pucks (pucks are heavy)
|
|
|
+ collide(ball, BALL_R, p1, PUCK_R, 5);
|
|
|
+ collide(ball, BALL_R, p2, PUCK_R, 5);
|
|
|
+
|
|
|
+ // Ball vs free biscuits
|
|
|
+ for (const b of biscs) {
|
|
|
+ if (b.stuck === null) collide(ball, BALL_R, b, BISC_R, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Free biscuit vs free biscuit
|
|
|
+ for (let i = 0; i < biscs.length; i++) {
|
|
|
+ for (let j = i + 1; j < biscs.length; j++) {
|
|
|
+ if (biscs[i].stuck === null && biscs[j].stuck === null) {
|
|
|
+ collide(biscs[i], BISC_R, biscs[j], BISC_R, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // Cap ball speed so it doesn't go crazy
|
|
|
- clampSpeed(ball, 7);
|
|
|
+ // --- Magnetic attachment: free biscuit near puck → sticks ---
|
|
|
+ for (const b of biscs) {
|
|
|
+ if (b.stuck !== null) continue;
|
|
|
+ // Check puck 1
|
|
|
+ if (d(b.x, b.y, p1.x, p1.y) < ATTACH_DIST) {
|
|
|
+ b.stuck = 1;
|
|
|
+ b.angle = Math.atan2(b.y - p1.y, b.x - p1.x);
|
|
|
+ p1.attached++;
|
|
|
+ }
|
|
|
+ // Check puck 2
|
|
|
+ else if (d(b.x, b.y, p2.x, p2.y) < ATTACH_DIST) {
|
|
|
+ b.stuck = 2;
|
|
|
+ b.angle = Math.atan2(b.y - p2.y, b.x - p2.x);
|
|
|
+ p2.attached++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2 biscuits stuck = point scored, reset round ---
|
|
|
+ if (p1.attached >= 2 || p2.attached >= 2) {
|
|
|
+ // Brief pause then reset
|
|
|
+ setTimeout(() => resetRound(), 800);
|
|
|
+ // Stop this tick cycle — resetRound will let a new one start
|
|
|
+ p1.attached = 0; p2.attached = 0;
|
|
|
+ resetRound();
|
|
|
+ }
|
|
|
|
|
|
- // Apply transforms
|
|
|
+ // Ball vs stuck biscuits (they act as part of the puck now)
|
|
|
+ for (const b of biscs) {
|
|
|
+ if (b.stuck !== null) collide(ball, BALL_R, b, BISC_R, 5);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Free biscuit vs pucks (pucks heavy, but don't attach yet — that's handled above)
|
|
|
+ for (const b of biscs) {
|
|
|
+ if (b.stuck === null) {
|
|
|
+ collide(b, BISC_R, p1, PUCK_R, 5);
|
|
|
+ collide(b, BISC_R, p2, PUCK_R, 5);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Goal detection — ball enters a goal, reset round
|
|
|
+ if (d(ball.x, ball.y, GOAL_L.x, GOAL_L.y) < GOAL_L.r - 4 ||
|
|
|
+ d(ball.x, ball.y, GOAL_R.x, GOAL_R.y) < GOAL_R.r - 4) {
|
|
|
+ resetRound();
|
|
|
+ }
|
|
|
+
|
|
|
+ capSpeed(ball, 7);
|
|
|
+ for (const b of biscs) { if (b.stuck === null) capSpeed(b, 5); }
|
|
|
+
|
|
|
+ // Render
|
|
|
ballEl.setAttribute("transform", `translate(${ball.x.toFixed(1)},${ball.y.toFixed(1)})`);
|
|
|
p1El.setAttribute("transform", `translate(${p1.x.toFixed(1)},${p1.y.toFixed(1)})`);
|
|
|
p2El.setAttribute("transform", `translate(${p2.x.toFixed(1)},${p2.y.toFixed(1)})`);
|
|
|
+ bEls.forEach((el, i) => el.setAttribute("transform", `translate(${biscs[i].x.toFixed(1)},${biscs[i].y.toFixed(1)})`));
|
|
|
|
|
|
requestAnimationFrame(tick);
|
|
|
}
|
|
|
|
|
|
// Init positions
|
|
|
- resetBall();
|
|
|
p1El.setAttribute("transform", `translate(${p1.x},${p1.y})`);
|
|
|
p2El.setAttribute("transform", `translate(${p2.x},${p2.y})`);
|
|
|
+ bEls.forEach((el, i) => el.setAttribute("transform", `translate(${biscs[i].x},${biscs[i].y})`));
|
|
|
requestAnimationFrame(tick);
|
|
|
})();
|
|
|
</script>
|