Add Physilinks prototype with scoring and game over
This commit is contained in:
305
index.html
Normal file
305
index.html
Normal file
@@ -0,0 +1,305 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Physilinks</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--panel: #111827;
|
||||
--accent: #22d3ee;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
"Manrope",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 25% 20%,
|
||||
rgba(56, 189, 248, 0.12),
|
||||
transparent 25%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 10%,
|
||||
rgba(167, 139, 250, 0.16),
|
||||
transparent 30%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 40% 80%,
|
||||
rgba(52, 211, 153, 0.12),
|
||||
transparent 25%
|
||||
),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.shell {
|
||||
width: min(1100px, 100%);
|
||||
background: rgba(17, 24, 39, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.1);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
header h1 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
header .meta {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
header .pill {
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(34, 211, 238, 0.12);
|
||||
color: #67e8f9;
|
||||
border: 1px solid rgba(34, 211, 238, 0.35);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#scene-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 680px;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 30% 30%,
|
||||
rgba(255, 255, 255, 0.02),
|
||||
transparent 40%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 75% 60%,
|
||||
rgba(255, 255, 255, 0.02),
|
||||
transparent 45%
|
||||
),
|
||||
#0b1222;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.hud-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
.hud-bar .card {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.hud-bar .card strong {
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
.legend {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.legend span {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
display: inline-block;
|
||||
}
|
||||
.game-over {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 13, 25, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
.game-over.visible {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
.game-over__card {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 14px;
|
||||
padding: 20px 24px;
|
||||
text-align: center;
|
||||
min-width: 240px;
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.game-over__card .title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.game-over__card .score-line {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.game-over__card button {
|
||||
background: linear-gradient(135deg, #22d3ee, #0ea5e9);
|
||||
border: none;
|
||||
color: #0b1222;
|
||||
font-weight: 700;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.game-over__card button:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
.floating-score {
|
||||
position: absolute;
|
||||
color: #e0f2fe;
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
|
||||
animation: floatUp 900ms ease-out forwards;
|
||||
}
|
||||
@keyframes floatUp {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
60% {
|
||||
opacity: 0.9;
|
||||
transform: translate(-50%, -22px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -38px);
|
||||
}
|
||||
}
|
||||
.instructions {
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.08);
|
||||
line-height: 1.6;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
#scene-wrapper {
|
||||
height: 520px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<h1>Physilinks</h1>
|
||||
<div class="meta">
|
||||
<span class="pill">Physics</span>
|
||||
<span>Link same-colored balls to clear them.</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="scene-wrapper">
|
||||
<div class="legend" id="palette-legend"></div>
|
||||
<div class="game-over" id="game-over">
|
||||
<div class="game-over__card">
|
||||
<div class="title">Game Over</div>
|
||||
<div class="score-line">
|
||||
Final score: <span id="final-score">0</span>
|
||||
</div>
|
||||
<button id="restart-btn">Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud-bar">
|
||||
<div class="card">
|
||||
<strong>Spawn</strong> <span id="spawn-rate"></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<strong>Min link</strong> <span id="min-link"></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<strong>Active color</strong>
|
||||
<span id="active-color">—</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<strong>Chain length</strong>
|
||||
<span id="chain-length">0</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<strong>Score</strong> <span id="score">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
Click or touch a ball to start a chain. Drag through balls of
|
||||
the same color to link them together. Drag back to the previous
|
||||
ball to undo the last link. Release to finish: chains of fewer
|
||||
than the minimum vanish; chains with enough balls clear all
|
||||
linked balls (score: 10 × length²). If the entry gets blocked
|
||||
and a new ball cannot drop in, the run ends—restart to try
|
||||
again.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/matter-js@0.19.0/build/matter.min.js"></script>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
413
main.js
Normal file
413
main.js
Normal file
@@ -0,0 +1,413 @@
|
||||
(() => {
|
||||
const {
|
||||
Engine,
|
||||
Render,
|
||||
Runner,
|
||||
World,
|
||||
Bodies,
|
||||
Constraint,
|
||||
Events,
|
||||
Query,
|
||||
Vector,
|
||||
} = Matter;
|
||||
|
||||
const config = {
|
||||
gravity: 0.78,
|
||||
spawnIntervalMs: 720,
|
||||
minChain: 3,
|
||||
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
|
||||
ballRadius: 48,
|
||||
};
|
||||
|
||||
const sceneEl = document.getElementById("scene-wrapper");
|
||||
const activeColorEl = document.getElementById("active-color");
|
||||
const chainLenEl = document.getElementById("chain-length");
|
||||
const spawnRateEl = document.getElementById("spawn-rate");
|
||||
const minLinkEl = document.getElementById("min-link");
|
||||
const paletteLegendEl = document.getElementById("palette-legend");
|
||||
const scoreEl = document.getElementById("score");
|
||||
const gameOverEl = document.getElementById("game-over");
|
||||
const finalScoreEl = document.getElementById("final-score");
|
||||
const restartBtn = document.getElementById("restart-btn");
|
||||
|
||||
let width = sceneEl.clientWidth;
|
||||
let height = sceneEl.clientHeight;
|
||||
|
||||
const engine = Engine.create();
|
||||
engine.gravity.y = config.gravity;
|
||||
const world = engine.world;
|
||||
|
||||
const render = Render.create({
|
||||
element: sceneEl,
|
||||
engine,
|
||||
options: {
|
||||
width,
|
||||
height,
|
||||
wireframes: false,
|
||||
background: "transparent",
|
||||
showAngleIndicator: false,
|
||||
pixelRatio: window.devicePixelRatio || 1,
|
||||
},
|
||||
});
|
||||
Render.run(render);
|
||||
|
||||
const runner = Runner.create();
|
||||
Runner.run(runner, engine);
|
||||
|
||||
// Static boundaries and some obstacles to bounce around.
|
||||
let boundaries = [];
|
||||
const createBounds = () => {
|
||||
boundaries.forEach((b) => World.remove(world, b));
|
||||
boundaries = [
|
||||
Bodies.rectangle(width / 2, height + 40, width, 80, {
|
||||
isStatic: true,
|
||||
restitution: 0.8,
|
||||
}),
|
||||
Bodies.rectangle(-40, height / 2, 80, height * 2, { isStatic: true }),
|
||||
Bodies.rectangle(width + 40, height / 2, 80, height * 2, {
|
||||
isStatic: true,
|
||||
}),
|
||||
];
|
||||
boundaries.push(
|
||||
Bodies.rectangle(width * 0.25, height * 0.55, 160, 20, {
|
||||
isStatic: true,
|
||||
angle: -0.3,
|
||||
}),
|
||||
Bodies.rectangle(width * 0.7, height * 0.4, 220, 24, {
|
||||
isStatic: true,
|
||||
angle: 0.26,
|
||||
}),
|
||||
);
|
||||
World.add(world, boundaries);
|
||||
};
|
||||
createBounds();
|
||||
|
||||
const balls = [];
|
||||
let spawnTimer = null;
|
||||
let score = 0;
|
||||
let gameOver = false;
|
||||
|
||||
const chain = {
|
||||
active: false,
|
||||
color: null,
|
||||
bodies: [],
|
||||
constraints: [],
|
||||
pointer: null,
|
||||
};
|
||||
|
||||
const spawnBall = () => {
|
||||
if (gameOver) return;
|
||||
const color =
|
||||
config.palette[Math.floor(Math.random() * config.palette.length)];
|
||||
const x = Math.max(
|
||||
config.ballRadius + 10,
|
||||
Math.min(width - config.ballRadius - 10, Math.random() * width),
|
||||
);
|
||||
const y = -config.ballRadius * 2;
|
||||
const ball = Bodies.circle(x, y, config.ballRadius, {
|
||||
restitution: 0.72,
|
||||
friction: 0.01,
|
||||
frictionAir: 0.015,
|
||||
render: {
|
||||
fillStyle: color,
|
||||
strokeStyle: "#0b1222",
|
||||
lineWidth: 2,
|
||||
},
|
||||
});
|
||||
ball.plugin = { color, hasEntered: false, entryCheckId: null };
|
||||
balls.push(ball);
|
||||
World.add(world, ball);
|
||||
ball.plugin.entryCheckId = setTimeout(() => {
|
||||
ball.plugin.entryCheckId = null;
|
||||
if (gameOver) return;
|
||||
if (!ball.plugin.hasEntered && Math.abs(ball.velocity.y) < 0.2) {
|
||||
triggerGameOver();
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const startSpawner = () => {
|
||||
if (spawnTimer) clearInterval(spawnTimer);
|
||||
spawnTimer = setInterval(spawnBall, config.spawnIntervalMs);
|
||||
};
|
||||
|
||||
const cleanupBall = (ball) => {
|
||||
if (ball.plugin && ball.plugin.entryCheckId) {
|
||||
clearTimeout(ball.plugin.entryCheckId);
|
||||
ball.plugin.entryCheckId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const triggerGameOver = () => {
|
||||
if (gameOver) return;
|
||||
gameOver = true;
|
||||
resetChainVisuals();
|
||||
if (spawnTimer) clearInterval(spawnTimer);
|
||||
finalScoreEl.textContent = score;
|
||||
gameOverEl.classList.add("visible");
|
||||
};
|
||||
|
||||
const restartGame = () => {
|
||||
gameOver = false;
|
||||
score = 0;
|
||||
resetChainVisuals();
|
||||
balls.forEach((ball) => {
|
||||
cleanupBall(ball);
|
||||
World.remove(world, ball);
|
||||
});
|
||||
balls.length = 0;
|
||||
gameOverEl.classList.remove("visible");
|
||||
updateHud();
|
||||
startSpawner();
|
||||
};
|
||||
|
||||
const setHighlight = (body, on) => {
|
||||
body.render.lineWidth = on ? 4 : 2;
|
||||
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
||||
};
|
||||
|
||||
const resetChainVisuals = () => {
|
||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||
chain.constraints.forEach((c) => World.remove(world, c));
|
||||
chain.active = false;
|
||||
chain.color = null;
|
||||
chain.bodies = [];
|
||||
chain.constraints = [];
|
||||
chain.pointer = null;
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const removeLastFromChain = () => {
|
||||
const removedConstraint = chain.constraints.pop();
|
||||
if (removedConstraint) {
|
||||
World.remove(world, removedConstraint);
|
||||
}
|
||||
const removedBody = chain.bodies.pop();
|
||||
if (removedBody) setHighlight(removedBody, false);
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const addToChain = (body) => {
|
||||
const last = chain.bodies[chain.bodies.length - 1];
|
||||
const dist = Vector.magnitude(Vector.sub(last.position, body.position));
|
||||
const constraint = Constraint.create({
|
||||
bodyA: last,
|
||||
bodyB: body,
|
||||
length: dist,
|
||||
stiffness: 0.9,
|
||||
render: {
|
||||
strokeStyle: chain.color,
|
||||
lineWidth: 3,
|
||||
},
|
||||
});
|
||||
chain.constraints.push(constraint);
|
||||
chain.bodies.push(body);
|
||||
setHighlight(body, true);
|
||||
World.add(world, constraint);
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const spawnScorePopup = (point, amount, color) => {
|
||||
if (!point) return;
|
||||
const el = document.createElement("div");
|
||||
el.className = "floating-score";
|
||||
el.textContent = `+${amount}`;
|
||||
el.style.left = `${point.x}px`;
|
||||
el.style.top = `${point.y}px`;
|
||||
el.style.color = color || "#e0f2fe";
|
||||
sceneEl.appendChild(el);
|
||||
setTimeout(() => el.remove(), 950);
|
||||
};
|
||||
|
||||
const finishChain = (releasePoint) => {
|
||||
if (!chain.active || gameOver) return;
|
||||
if (chain.bodies.length >= config.minChain) {
|
||||
const gain = 10 * Math.pow(chain.bodies.length, 2);
|
||||
score += gain;
|
||||
spawnScorePopup(releasePoint || chain.pointer, gain, chain.color);
|
||||
chain.constraints.forEach((c) => World.remove(world, c));
|
||||
chain.bodies.forEach((body) => {
|
||||
cleanupBall(body);
|
||||
World.remove(world, body);
|
||||
});
|
||||
// Remove cleared balls from tracking list.
|
||||
for (let i = balls.length - 1; i >= 0; i -= 1) {
|
||||
if (chain.bodies.includes(balls[i])) {
|
||||
balls.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chain.constraints.forEach((c) => World.remove(world, c));
|
||||
chain.bodies.forEach((b) => setHighlight(b, false));
|
||||
}
|
||||
chain.active = false;
|
||||
chain.color = null;
|
||||
chain.bodies = [];
|
||||
chain.constraints = [];
|
||||
chain.pointer = null;
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const pickBody = (point) => {
|
||||
const hits = Query.point(balls, point);
|
||||
return hits[0];
|
||||
};
|
||||
|
||||
const getPointerPosition = (evt) => {
|
||||
const rect = render.canvas.getBoundingClientRect();
|
||||
const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
|
||||
const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY;
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top,
|
||||
};
|
||||
};
|
||||
|
||||
const handlePointerDown = (evt) => {
|
||||
if (gameOver) return;
|
||||
const point = getPointerPosition(evt);
|
||||
const body = pickBody(point);
|
||||
if (!body) return;
|
||||
chain.active = true;
|
||||
chain.color = body.plugin.color;
|
||||
chain.bodies = [body];
|
||||
chain.constraints = [];
|
||||
chain.pointer = point;
|
||||
setHighlight(body, true);
|
||||
updateHud();
|
||||
};
|
||||
|
||||
const handlePointerMove = (evt) => {
|
||||
if (!chain.active) return;
|
||||
if (gameOver) return;
|
||||
const point = getPointerPosition(evt);
|
||||
chain.pointer = point;
|
||||
const body = pickBody(point);
|
||||
if (!body) return;
|
||||
const alreadyInChain = chain.bodies.includes(body);
|
||||
if (alreadyInChain) {
|
||||
const targetIndex = chain.bodies.indexOf(body);
|
||||
if (chain.bodies.length > 1 && targetIndex === chain.bodies.length - 2) {
|
||||
removeLastFromChain();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (body.plugin.color !== chain.color) return;
|
||||
addToChain(body);
|
||||
};
|
||||
|
||||
const handlePointerUp = () => finishChain(chain.pointer);
|
||||
|
||||
render.canvas.addEventListener("mousedown", handlePointerDown);
|
||||
render.canvas.addEventListener("mousemove", handlePointerMove);
|
||||
window.addEventListener("mouseup", handlePointerUp);
|
||||
render.canvas.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
handlePointerDown(e);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
render.canvas.addEventListener(
|
||||
"touchmove",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
handlePointerMove(e);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
render.canvas.addEventListener(
|
||||
"touchend",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
handlePointerUp(e);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
const updateHud = () => {
|
||||
spawnRateEl.textContent = `${config.spawnIntervalMs} ms`;
|
||||
minLinkEl.textContent = config.minChain;
|
||||
chainLenEl.textContent = chain.bodies.length;
|
||||
scoreEl.textContent = score;
|
||||
if (chain.color) {
|
||||
activeColorEl.textContent = "";
|
||||
activeColorEl.style.display = "inline-block";
|
||||
activeColorEl.style.width = "14px";
|
||||
activeColorEl.style.height = "14px";
|
||||
activeColorEl.style.borderRadius = "50%";
|
||||
activeColorEl.style.background = chain.color;
|
||||
activeColorEl.style.border = "1px solid rgba(255,255,255,0.3)";
|
||||
} else {
|
||||
activeColorEl.removeAttribute("style");
|
||||
activeColorEl.textContent = "—";
|
||||
}
|
||||
};
|
||||
|
||||
const buildLegend = () => {
|
||||
paletteLegendEl.innerHTML = "";
|
||||
config.palette.forEach((color) => {
|
||||
const swatch = document.createElement("span");
|
||||
swatch.style.background = color;
|
||||
paletteLegendEl.appendChild(swatch);
|
||||
});
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
width = sceneEl.clientWidth;
|
||||
height = sceneEl.clientHeight;
|
||||
render.canvas.width = width;
|
||||
render.canvas.height = height;
|
||||
render.options.width = width;
|
||||
render.options.height = height;
|
||||
Render.lookAt(render, {
|
||||
min: { x: 0, y: 0 },
|
||||
max: { x: width, y: height },
|
||||
});
|
||||
createBounds();
|
||||
};
|
||||
|
||||
Events.on(engine, "afterUpdate", () => {
|
||||
// Keep stray balls within the play area horizontally.
|
||||
balls.forEach((ball) => {
|
||||
if (
|
||||
!ball.plugin.hasEntered &&
|
||||
ball.position.y > config.ballRadius * 1.5
|
||||
) {
|
||||
ball.plugin.hasEntered = true;
|
||||
}
|
||||
if (
|
||||
ball.position.x < -100 ||
|
||||
ball.position.x > width + 100 ||
|
||||
ball.position.y > height + 500
|
||||
) {
|
||||
cleanupBall(ball);
|
||||
ball.plugin.hasEntered = true;
|
||||
Matter.Body.setPosition(ball, { x: Math.random() * width, y: -40 });
|
||||
Matter.Body.setVelocity(ball, { x: 0, y: 0 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Events.on(render, "afterRender", () => {
|
||||
if (!chain.active || !chain.pointer || chain.bodies.length === 0) return;
|
||||
const last = chain.bodies[chain.bodies.length - 1];
|
||||
const ctx = render.context;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = chain.color || "#fff";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.setLineDash([6, 6]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(last.position.x, last.position.y);
|
||||
ctx.lineTo(chain.pointer.x, chain.pointer.y);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
restartBtn.addEventListener("click", restartGame);
|
||||
buildLegend();
|
||||
updateHud();
|
||||
startSpawner();
|
||||
})();
|
||||
Reference in New Issue
Block a user