Move JavaScript files into src folder
This commit is contained in:
5
src/decomp-setup.js
Normal file
5
src/decomp-setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(() => {
|
||||
if (typeof Matter !== "undefined" && Matter.Common && window.decomp) {
|
||||
Matter.Common.setDecomp(window.decomp);
|
||||
}
|
||||
})();
|
||||
1059
src/main.js
Normal file
1059
src/main.js
Normal file
File diff suppressed because it is too large
Load Diff
28
src/scenes/index.js
Normal file
28
src/scenes/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
(() => {
|
||||
const scenes = window.PhysilinksSceneDefs || [];
|
||||
const desiredOrder = [
|
||||
"scene-grid",
|
||||
"low-g-terraces",
|
||||
"fast-drop-maze",
|
||||
"balanced",
|
||||
"scene-lava",
|
||||
"relax",
|
||||
];
|
||||
const orderedScenes = desiredOrder
|
||||
.map((id) => scenes.find((s) => s.id === id))
|
||||
.filter(Boolean);
|
||||
const unordered = scenes.filter(
|
||||
(s) => !orderedScenes.find((o) => o.id === s.id),
|
||||
);
|
||||
const finalScenes = [...orderedScenes, ...unordered];
|
||||
const defaultSceneId =
|
||||
desiredOrder.find((id) => finalScenes.some((s) => s.id === id)) ||
|
||||
finalScenes[0]?.id ||
|
||||
"scene-grid";
|
||||
|
||||
window.PhysilinksScenes = {
|
||||
scenes: finalScenes,
|
||||
defaultSceneId,
|
||||
order: desiredOrder,
|
||||
};
|
||||
})();
|
||||
90
src/scenes/scene-balanced.js
Normal file
90
src/scenes/scene-balanced.js
Normal file
@@ -0,0 +1,90 @@
|
||||
(() => {
|
||||
const { Bodies } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
scenes.push({
|
||||
id: "balanced",
|
||||
name: "Balanced",
|
||||
config: {
|
||||
gravity: 0.88,
|
||||
spawnIntervalMs: 520,
|
||||
minChain: 3,
|
||||
palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
|
||||
ballRadius: 38,
|
||||
winCondition: {
|
||||
type: "score",
|
||||
target: 10000,
|
||||
onWin: { shoveBalls: true },
|
||||
},
|
||||
link: {
|
||||
stiffness: 0.85,
|
||||
lengthScale: 1.05,
|
||||
damping: 0.08,
|
||||
lineWidth: 3,
|
||||
rope: true,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 3.2,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const floorHeight = Math.max(60, h * 0.12);
|
||||
const wallThickness = Math.max(32, w * 0.05);
|
||||
const wallHeight = h * 1.6;
|
||||
const platformHeight = Math.max(14, h * 0.025);
|
||||
const smallPlatformWidth = Math.max(140, w * 0.18);
|
||||
const largePlatformWidth = Math.max(180, w * 0.22);
|
||||
return [
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
h + floorHeight / 2,
|
||||
w + wallThickness * 2,
|
||||
floorHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
restitution: 0.8,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(-wallThickness / 2, h / 2, wallThickness, wallHeight, {
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#f97316", strokeStyle: "#f97316" },
|
||||
}),
|
||||
Bodies.rectangle(
|
||||
w + wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
wallHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#f97316", strokeStyle: "#f97316" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
w * 0.25,
|
||||
h * 0.55,
|
||||
smallPlatformWidth,
|
||||
platformHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
angle: -0.3,
|
||||
render: { fillStyle: "#22c55e", strokeStyle: "#22c55e" },
|
||||
plugin: { rotSpeed: 0.1 },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
w * 0.7,
|
||||
h * 0.4,
|
||||
largePlatformWidth,
|
||||
platformHeight * 1.2,
|
||||
{
|
||||
isStatic: true,
|
||||
angle: 0.26,
|
||||
render: { fillStyle: "#a855f7", strokeStyle: "#a855f7" },
|
||||
plugin: { rotSpeed: -0.08 },
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
97
src/scenes/scene-fastdrop.js
Normal file
97
src/scenes/scene-fastdrop.js
Normal file
@@ -0,0 +1,97 @@
|
||||
(() => {
|
||||
const { Bodies } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
scenes.push({
|
||||
id: "fast-drop-maze",
|
||||
name: "Fast drop maze",
|
||||
config: {
|
||||
gravity: 1.2,
|
||||
spawnIntervalMs: 220,
|
||||
minChain: 3,
|
||||
palette: ["#e879f9", "#38bdf8", "#f97316", "#22c55e"],
|
||||
ballRadius: 16,
|
||||
winCondition: {
|
||||
type: "colorClear",
|
||||
targets: [{ color: "#e879f9", count: 100 }],
|
||||
onWin: { setGravity: -0.5, swirlBalls: true },
|
||||
},
|
||||
link: {
|
||||
stiffness: 1,
|
||||
lengthScale: 0.85,
|
||||
damping: 0.15,
|
||||
lineWidth: 3,
|
||||
rope: false,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 23.8,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const floorHeight = Math.max(60, h * 0.1);
|
||||
const wallThickness = Math.max(28, w * 0.045);
|
||||
const wallHeight = h * 1.5;
|
||||
const pegRadius = Math.max(10, Math.min(22, Math.min(w, h) * 0.03));
|
||||
const platformHeight = Math.max(12, h * 0.02);
|
||||
const leftPlatformWidth = Math.max(120, w * 0.16);
|
||||
const rightPlatformWidth = Math.max(130, w * 0.18);
|
||||
const bodies = [
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
h + floorHeight / 2,
|
||||
w + wallThickness * 2,
|
||||
floorHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
restitution: 0.75,
|
||||
render: { fillStyle: "#14b8a6", strokeStyle: "#14b8a6" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(-wallThickness / 2, h / 2, wallThickness, wallHeight, {
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#f472b6", strokeStyle: "#f472b6" },
|
||||
}),
|
||||
Bodies.rectangle(
|
||||
w + wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
wallHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#f472b6", strokeStyle: "#f472b6" },
|
||||
},
|
||||
),
|
||||
];
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const x = (w * (i + 1)) / 6;
|
||||
const y = h * 0.35 + (i % 2 === 0 ? h * 0.06 : -h * 0.05);
|
||||
bodies.push(
|
||||
Bodies.circle(x, y, pegRadius, {
|
||||
isStatic: true,
|
||||
restitution: 0.9,
|
||||
render: { fillStyle: "#facc15", strokeStyle: "#facc15" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
bodies.push(
|
||||
Bodies.rectangle(w * 0.3, h * 0.55, leftPlatformWidth, platformHeight, {
|
||||
isStatic: true,
|
||||
angle: -0.3,
|
||||
render: { fillStyle: "#8b5cf6", strokeStyle: "#8b5cf6" },
|
||||
}),
|
||||
Bodies.rectangle(
|
||||
w * 0.7,
|
||||
h * 0.58,
|
||||
rightPlatformWidth,
|
||||
platformHeight * 1.05,
|
||||
{
|
||||
isStatic: true,
|
||||
angle: 0.28,
|
||||
render: { fillStyle: "#10b981", strokeStyle: "#10b981" },
|
||||
},
|
||||
),
|
||||
);
|
||||
return bodies;
|
||||
},
|
||||
});
|
||||
})();
|
||||
129
src/scenes/scene-grid.js
Normal file
129
src/scenes/scene-grid.js
Normal file
@@ -0,0 +1,129 @@
|
||||
(() => {
|
||||
const { Bodies } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
scenes.push({
|
||||
id: "scene-grid",
|
||||
name: "Zero-G Grid (default)",
|
||||
config: {
|
||||
gravity: 0,
|
||||
spawnIntervalMs: 0,
|
||||
autoSpawn: false,
|
||||
minChain: 3,
|
||||
palette: ["#38bdf8", "#f472b6", "#facc15", "#34d399", "#a78bfa"],
|
||||
ballRadius: 24,
|
||||
gridPadding: 0.08, // percent of viewport padding applied to both axes
|
||||
gridBallScale: 0.38, // percent of cell size used as radius
|
||||
gridLegend: {
|
||||
A: "#38bdf8",
|
||||
B: "#f472b6",
|
||||
C: "#facc15",
|
||||
D: "#34d399",
|
||||
E: "#a78bfa",
|
||||
},
|
||||
winCondition: {
|
||||
type: "clearCount",
|
||||
target: 25,
|
||||
nextSceneId: "scene1",
|
||||
onWin: { setGravity: 0.88 },
|
||||
},
|
||||
gridLayouts: [
|
||||
[
|
||||
"AABBBBAA",
|
||||
"ACCCBCCA",
|
||||
"ACDDDDCA",
|
||||
"ACEEEECA",
|
||||
"ACEEEECA",
|
||||
"ACDDDDCA",
|
||||
"ACCCBCCA",
|
||||
"AABBBBAA",
|
||||
],
|
||||
[
|
||||
"AAAABBBA",
|
||||
"ABBBCCCA",
|
||||
"ABCDDDCA",
|
||||
"ABCEEECA",
|
||||
"ABCEEECA",
|
||||
"ABCDDDCA",
|
||||
"ABBBCCCA",
|
||||
"AAAABBBA",
|
||||
],
|
||||
[
|
||||
"AABBCCDD",
|
||||
"ABBCCDEE",
|
||||
"ABCCDEEA",
|
||||
"ACCDDEEA",
|
||||
"ACCDDEEA",
|
||||
"ABCCDEEA",
|
||||
"ABBCCDEE",
|
||||
"AABBCCDD",
|
||||
],
|
||||
],
|
||||
link: {
|
||||
stiffness: 0.82,
|
||||
lengthScale: 1.05,
|
||||
damping: 0.06,
|
||||
lineWidth: 3,
|
||||
rope: true,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 3.8,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const pad = 0.08;
|
||||
const usableW = w * (1 - pad * 2);
|
||||
const usableH = h * (1 - pad * 2);
|
||||
const gridSize = Math.min(usableW, usableH);
|
||||
const gridX = (w - gridSize) / 2;
|
||||
const gridY = (h - gridSize) / 2;
|
||||
const wallThickness = Math.max(18, gridSize * 0.045);
|
||||
const innerW = gridSize;
|
||||
const innerH = gridSize;
|
||||
const cx = gridX + innerW / 2;
|
||||
const cy = gridY + innerH / 2;
|
||||
return [
|
||||
Bodies.rectangle(
|
||||
cx,
|
||||
gridY - wallThickness / 2,
|
||||
innerW + wallThickness * 2,
|
||||
wallThickness,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
cx,
|
||||
gridY + innerH + wallThickness / 2,
|
||||
innerW + wallThickness * 2,
|
||||
wallThickness,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
gridX - wallThickness / 2,
|
||||
cy,
|
||||
wallThickness,
|
||||
innerH + wallThickness * 2,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
gridX + innerW + wallThickness / 2,
|
||||
cy,
|
||||
wallThickness,
|
||||
innerH + wallThickness * 2,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#0b1222", strokeStyle: "#0b1222" },
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
96
src/scenes/scene-lavalamp.js
Normal file
96
src/scenes/scene-lavalamp.js
Normal file
@@ -0,0 +1,96 @@
|
||||
(() => {
|
||||
const { Bodies, Body } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
const makeCurveSegments = (cx, h, amp, thickness, segments) => {
|
||||
const segs = [];
|
||||
const stepY = h / segments;
|
||||
let prevX = cx;
|
||||
let prevY = 0;
|
||||
for (let i = 0; i < segments; i += 1) {
|
||||
const y = stepY * (i + 1);
|
||||
const t = y / h;
|
||||
const x = cx + Math.sin(t * Math.PI * 1.5) * amp;
|
||||
const dx = x - prevX;
|
||||
const dy = y - prevY;
|
||||
const len = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
||||
const angle = Math.atan2(dy, dx);
|
||||
segs.push(
|
||||
Bodies.rectangle((prevX + x) / 2, (prevY + y) / 2, thickness, len, {
|
||||
isStatic: true,
|
||||
angle,
|
||||
render: { fillStyle: "#14213a", strokeStyle: "#14213a" },
|
||||
plugin: { curve: true },
|
||||
}),
|
||||
);
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
return segs;
|
||||
};
|
||||
|
||||
scenes.push({
|
||||
id: "scene-lava",
|
||||
name: "Lava drift",
|
||||
config: {
|
||||
gravity: -0.1,
|
||||
spawnIntervalMs: 180,
|
||||
spawnFrom: "bottom",
|
||||
autoSpawn: true,
|
||||
minChain: 3,
|
||||
palette: ["#f472b6", "#38bdf8", "#fbbf24", "#a855f7", "#22c55e"],
|
||||
ballRadius: 26,
|
||||
blobBalls: "jagged",
|
||||
winCondition: {
|
||||
type: "score",
|
||||
target: 25000,
|
||||
onWin: { setGravity: -0.55, removeCurves: true },
|
||||
},
|
||||
link: {
|
||||
stiffness: 0.7,
|
||||
lengthScale: 1.05,
|
||||
damping: 0.12,
|
||||
lineWidth: 3,
|
||||
rope: true,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 5.8,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const wallThickness = Math.max(24, w * 0.05);
|
||||
const amp = Math.max(40, w * 0.08);
|
||||
const segments = 12;
|
||||
const curves = [
|
||||
...makeCurveSegments(w * 0.33, h, amp, wallThickness, segments),
|
||||
...makeCurveSegments(w * 0.67, h, amp * 0.95, wallThickness, segments),
|
||||
];
|
||||
|
||||
// Gentle paddles that sway slightly
|
||||
/*
|
||||
const paddleWidth = Math.max(120, w * 0.18);
|
||||
const paddleHeight = Math.max(12, h * 0.018);
|
||||
const paddles = [
|
||||
Bodies.rectangle(w * 0.45, h * 0.65, paddleWidth, paddleHeight, {
|
||||
isStatic: true,
|
||||
angle: -0.08,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
plugin: {
|
||||
oscillate: { axis: "x", amplitude: w * 0.05, speed: 0.7 },
|
||||
},
|
||||
}),
|
||||
Bodies.rectangle(w * 0.58, h * 0.4, paddleWidth * 0.85, paddleHeight, {
|
||||
isStatic: true,
|
||||
angle: 0.12,
|
||||
render: { fillStyle: "#f59e0b", strokeStyle: "#f59e0b" },
|
||||
plugin: {
|
||||
oscillate: { axis: "x", amplitude: w * 0.04, speed: 1.0 },
|
||||
},
|
||||
}),
|
||||
];
|
||||
*/
|
||||
|
||||
return [...curves];
|
||||
},
|
||||
});
|
||||
})();
|
||||
194
src/scenes/scene-lowg.js
Normal file
194
src/scenes/scene-lowg.js
Normal file
@@ -0,0 +1,194 @@
|
||||
(() => {
|
||||
const { Bodies, Body, Vertices } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
scenes.push({
|
||||
id: "low-g-terraces",
|
||||
name: "Low-G terraces",
|
||||
config: {
|
||||
gravity: 0.65,
|
||||
spawnIntervalMs: 600,
|
||||
minChain: 3,
|
||||
palette: ["#fb7185", "#fbbf24", "#34d399", "#38bdf8"],
|
||||
ballRadius: 22,
|
||||
winCondition: {
|
||||
type: "score",
|
||||
target: 15000,
|
||||
onWin: { shoveBalls: true },
|
||||
},
|
||||
link: {
|
||||
stiffness: 0.6,
|
||||
lengthScale: 1,
|
||||
damping: 0.01,
|
||||
lineWidth: 4,
|
||||
rope: false,
|
||||
renderType: "spring",
|
||||
maxLengthMultiplier: 6,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const floorHeight = Math.max(70, h * 0.12);
|
||||
const wallThickness = Math.max(32, w * 0.05);
|
||||
const wallHeight = h * 1.8;
|
||||
const cogRadius = Math.max(40, Math.min(w, h) * 0.085);
|
||||
const cogRadiusSmall = Math.max(30, Math.min(w, h) * 0.065);
|
||||
const bumperRadius = Math.max(20, Math.min(w, h) * 0.05);
|
||||
const stickyWidth = Math.max(90, w * 0.12);
|
||||
const stickyHeight = Math.max(14, h * 0.02);
|
||||
const stickyAmplitude = w * 0.06;
|
||||
const bumperAmplitude = h * 0.08;
|
||||
|
||||
const makeGear = (cx, cy, outerRadius, teeth, color, rotSpeed) => {
|
||||
const coreRadius = outerRadius * 1.5;
|
||||
const toothLength = outerRadius * 0.5;
|
||||
const toothWidth = Math.max(outerRadius * 0.14, 10);
|
||||
const parts = [
|
||||
Bodies.circle(cx, cy, coreRadius, {
|
||||
isStatic: true,
|
||||
render: { fillStyle: color, strokeStyle: color },
|
||||
}),
|
||||
];
|
||||
const step = (Math.PI * 2) / teeth;
|
||||
for (let i = 0; i < teeth; i += 1) {
|
||||
const angle = step * i;
|
||||
const tx = cx + Math.cos(angle) * (coreRadius + toothLength / 2);
|
||||
const ty = cy + Math.sin(angle) * (coreRadius + toothLength / 2);
|
||||
parts.push(
|
||||
Bodies.rectangle(tx, ty, toothWidth, toothLength, {
|
||||
isStatic: true,
|
||||
angle,
|
||||
render: { fillStyle: color, strokeStyle: color },
|
||||
}),
|
||||
);
|
||||
}
|
||||
const gear = Body.create({
|
||||
isStatic: true,
|
||||
parts,
|
||||
plugin: { rotSpeed },
|
||||
});
|
||||
return gear;
|
||||
};
|
||||
|
||||
const makeStar = (x, y, size, color, angle, rotSpeed) => {
|
||||
const base = Vertices.fromPath(
|
||||
"50 0 63 38 100 38 69 59 82 100 50 75 18 100 31 59 0 38 37 38",
|
||||
);
|
||||
const scale = (size || 50) / 50;
|
||||
const pts = base.map((p) => ({
|
||||
x: (p.x - 50) * scale,
|
||||
y: (p.y - 50) * scale,
|
||||
}));
|
||||
return Bodies.fromVertices(
|
||||
x,
|
||||
y,
|
||||
[pts],
|
||||
{
|
||||
isStatic: true,
|
||||
angle,
|
||||
render: { fillStyle: color, strokeStyle: color },
|
||||
plugin: { rotSpeed },
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
h + floorHeight / 2,
|
||||
w + wallThickness * 2,
|
||||
floorHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
restitution: 0.9,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(-wallThickness / 2, h / 2, wallThickness, wallHeight, {
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#7c3aed", strokeStyle: "#7c3aed" },
|
||||
}),
|
||||
Bodies.rectangle(
|
||||
w + wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
wallHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#7c3aed", strokeStyle: "#7c3aed" },
|
||||
},
|
||||
),
|
||||
// Rotating cogs with teeth
|
||||
makeGear(w * 0.32, h * 0.38, cogRadius, 10, "#f97316", 1.2),
|
||||
makeGear(w * 0.64, h * 0.5, cogRadiusSmall, 12, "#a855f7", -1.6),
|
||||
makeGear(w * 0.5, h * 0.32, cogRadius * 0.85, 14, "#fb7185", 1.9),
|
||||
// Oscillating bumpers
|
||||
Bodies.circle(w * 0.2, h * 0.46, bumperRadius, {
|
||||
isStatic: true,
|
||||
restitution: 1.08,
|
||||
friction: 0.01,
|
||||
render: { fillStyle: "#14b8a6", strokeStyle: "#14b8a6" },
|
||||
plugin: {
|
||||
oscillate: { axis: "y", amplitude: bumperAmplitude, speed: 1.4 },
|
||||
},
|
||||
}),
|
||||
Bodies.circle(w * 0.5, h * 0.62, bumperRadius * 0.9, {
|
||||
isStatic: true,
|
||||
restitution: 1.08,
|
||||
friction: 0.01,
|
||||
render: { fillStyle: "#fbbf24", strokeStyle: "#fbbf24" },
|
||||
plugin: {
|
||||
oscillate: { axis: "x", amplitude: stickyAmplitude, speed: 1.1 },
|
||||
},
|
||||
}),
|
||||
Bodies.circle(w * 0.8, h * 0.38, bumperRadius * 1.05, {
|
||||
isStatic: true,
|
||||
restitution: 1.08,
|
||||
friction: 0.01,
|
||||
render: { fillStyle: "#38bdf8", strokeStyle: "#38bdf8" },
|
||||
plugin: {
|
||||
oscillate: {
|
||||
axis: "y",
|
||||
amplitude: bumperAmplitude * 0.7,
|
||||
speed: 1.8,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Sticky moving pads
|
||||
Bodies.rectangle(w * 0.32, h * 0.72, stickyWidth, stickyHeight, {
|
||||
isStatic: true,
|
||||
angle: -0.08,
|
||||
friction: 1.3,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
plugin: {
|
||||
oscillate: { axis: "x", amplitude: stickyAmplitude, speed: 0.9 },
|
||||
},
|
||||
}),
|
||||
Bodies.rectangle(w * 0.72, h * 0.7, stickyWidth * 0.9, stickyHeight, {
|
||||
isStatic: true,
|
||||
angle: 0.06,
|
||||
friction: 1.3,
|
||||
render: { fillStyle: "#f472b6", strokeStyle: "#f472b6" },
|
||||
plugin: {
|
||||
oscillate: {
|
||||
axis: "x",
|
||||
amplitude: stickyAmplitude * 0.8,
|
||||
speed: 1.3,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Star obstacles
|
||||
makeStar(w * 0.18, h * 0.28, bumperRadius * 1.1, "#22c55e", 0.2, -0.6),
|
||||
makeStar(
|
||||
w * 0.86,
|
||||
h * 0.62,
|
||||
bumperRadius * 1.15,
|
||||
"#c084fc",
|
||||
-0.25,
|
||||
0.9,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
128
src/scenes/scene-relax.js
Normal file
128
src/scenes/scene-relax.js
Normal file
@@ -0,0 +1,128 @@
|
||||
(() => {
|
||||
const { Bodies, Composites } = Matter;
|
||||
const scenes = (window.PhysilinksSceneDefs =
|
||||
window.PhysilinksSceneDefs || []);
|
||||
|
||||
scenes.push({
|
||||
id: "relax",
|
||||
name: "Relax drift",
|
||||
config: {
|
||||
gravity: 0.08,
|
||||
spawnIntervalMs: 850,
|
||||
spawnBatchMin: 3,
|
||||
spawnBatchMax: 5,
|
||||
spawnFrom: "bottom",
|
||||
autoSpawn: true,
|
||||
minChain: 2,
|
||||
palette: ["#38bdf8", "#f472b6", "#fbbf24", "#22c55e", "#a855f7"],
|
||||
ballRadius: 20,
|
||||
blobBalls: false,
|
||||
noGameOver: true,
|
||||
relaxMode: true,
|
||||
winCondition: {
|
||||
type: "timer",
|
||||
durationSec: 120,
|
||||
onWin: { setGravity: -0.4, swirlBalls: true },
|
||||
},
|
||||
link: {
|
||||
stiffness: 0.6,
|
||||
lengthScale: 1.1,
|
||||
damping: 0.1,
|
||||
lineWidth: 3,
|
||||
rope: true,
|
||||
renderType: "line",
|
||||
maxLengthMultiplier: 3.8,
|
||||
},
|
||||
},
|
||||
createBodies: (w, h) => {
|
||||
const wallThickness = Math.max(30, w * 0.04);
|
||||
const wallHeight = h + wallThickness * 2;
|
||||
const floorHeight = Math.max(40, h * 0.08);
|
||||
const bumperRadius = Math.max(30, Math.min(w, h) * 0.04);
|
||||
const makeSoft = (cx, cy, cols, rows, radius, color) => {
|
||||
const particleOpts = {
|
||||
friction: 0.02,
|
||||
frictionStatic: 0.04,
|
||||
restitution: 0.02,
|
||||
render: { fillStyle: color, strokeStyle: color },
|
||||
plugin: { draggable: true, nonLinkable: true },
|
||||
};
|
||||
const constraintOpts = {
|
||||
stiffness: 0.08,
|
||||
damping: 0.35,
|
||||
render: { visible: false, type: "line", anchors: false },
|
||||
};
|
||||
const comp = Composites.softBody(
|
||||
cx - cols * radius * 1.1,
|
||||
cy - rows * radius * 1.1,
|
||||
cols,
|
||||
rows,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
radius,
|
||||
particleOpts,
|
||||
constraintOpts,
|
||||
);
|
||||
comp.bodies.forEach((b) => {
|
||||
b.plugin = b.plugin || {};
|
||||
b.plugin.draggable = true;
|
||||
b.plugin.nonLinkable = true;
|
||||
});
|
||||
comp.constraints.forEach((c) => {
|
||||
c.plugin = { soft: true };
|
||||
});
|
||||
return comp;
|
||||
};
|
||||
|
||||
const softRadius = Math.max(18, w * 0.025);
|
||||
const softA = makeSoft(w * 0.32, h * 0.38, 3, 3, softRadius, "#38bdf8");
|
||||
const softB = makeSoft(w * 0.65, h * 0.55, 3, 3, softRadius, "#f472b6");
|
||||
const softC = makeSoft(w * 0.5, h * 0.32, 3, 3, softRadius, "#fbbf24");
|
||||
|
||||
return [
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
h + floorHeight / 2,
|
||||
w + wallThickness * 2,
|
||||
floorHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
restitution: 0.8,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(
|
||||
w / 2,
|
||||
-wallThickness / 2,
|
||||
w + wallThickness * 2,
|
||||
wallThickness,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#0ea5e9", strokeStyle: "#0ea5e9" },
|
||||
},
|
||||
),
|
||||
Bodies.rectangle(-wallThickness / 2, h / 2, wallThickness, wallHeight, {
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#7c3aed", strokeStyle: "#7c3aed" },
|
||||
}),
|
||||
Bodies.rectangle(
|
||||
w + wallThickness / 2,
|
||||
h / 2,
|
||||
wallThickness,
|
||||
wallHeight,
|
||||
{
|
||||
isStatic: true,
|
||||
render: { fillStyle: "#7c3aed", strokeStyle: "#7c3aed" },
|
||||
},
|
||||
),
|
||||
...softA.bodies,
|
||||
...softA.constraints,
|
||||
...softB.bodies,
|
||||
...softB.constraints,
|
||||
...softC.bodies,
|
||||
...softC.constraints,
|
||||
];
|
||||
},
|
||||
});
|
||||
})();
|
||||
216
src/ui.js
Normal file
216
src/ui.js
Normal file
@@ -0,0 +1,216 @@
|
||||
(() => {
|
||||
const create = () => {
|
||||
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 highScoreEl = document.getElementById("high-score");
|
||||
const sceneSelectEl = document.getElementById("scene-select");
|
||||
const gameOverEl = document.getElementById("game-over");
|
||||
const finalScoreEl = document.getElementById("final-score");
|
||||
const restartBtn = document.getElementById("restart-btn");
|
||||
const pauseBtn = document.getElementById("pause-btn");
|
||||
const pauseOverlay = document.getElementById("pause-overlay");
|
||||
const goalLabelEl = document.getElementById("goal-label");
|
||||
const goalProgressEl = document.getElementById("goal-progress");
|
||||
const winEl = document.getElementById("win-overlay");
|
||||
const winMessageEl = document.getElementById("win-message");
|
||||
const winNextBtn = document.getElementById("win-next");
|
||||
const winRestartBtn = document.getElementById("win-restart");
|
||||
|
||||
const handlers = {
|
||||
onPauseToggle: null,
|
||||
onRestart: null,
|
||||
onSceneChange: null,
|
||||
onWinNext: null,
|
||||
};
|
||||
|
||||
if (pauseBtn) {
|
||||
pauseBtn.addEventListener("click", () => {
|
||||
if (handlers.onPauseToggle) handlers.onPauseToggle();
|
||||
});
|
||||
}
|
||||
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener("click", () => {
|
||||
if (handlers.onRestart) handlers.onRestart();
|
||||
});
|
||||
}
|
||||
|
||||
if (sceneSelectEl) {
|
||||
sceneSelectEl.addEventListener("change", (e) => {
|
||||
if (handlers.onSceneChange) handlers.onSceneChange(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (winNextBtn) {
|
||||
winNextBtn.addEventListener("click", () => {
|
||||
if (handlers.onWinNext) handlers.onWinNext();
|
||||
});
|
||||
}
|
||||
|
||||
if (winRestartBtn) {
|
||||
winRestartBtn.addEventListener("click", () => {
|
||||
if (handlers.onRestart) handlers.onRestart();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && handlers.onPauseToggle) {
|
||||
handlers.onPauseToggle();
|
||||
}
|
||||
});
|
||||
|
||||
const setHandlers = (nextHandlers = {}) => {
|
||||
Object.assign(handlers, nextHandlers);
|
||||
};
|
||||
|
||||
const setSceneOptions = (scenes, activeId) => {
|
||||
if (!sceneSelectEl) return;
|
||||
sceneSelectEl.innerHTML = "";
|
||||
scenes.forEach((scene) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = scene.id;
|
||||
opt.textContent = scene.name;
|
||||
sceneSelectEl.appendChild(opt);
|
||||
});
|
||||
if (activeId) {
|
||||
sceneSelectEl.value = activeId;
|
||||
}
|
||||
};
|
||||
|
||||
const setSceneSelection = (sceneId) => {
|
||||
if (sceneSelectEl) {
|
||||
sceneSelectEl.value = sceneId;
|
||||
}
|
||||
};
|
||||
|
||||
const setPauseState = (paused) => {
|
||||
if (pauseOverlay) {
|
||||
pauseOverlay.classList.toggle("visible", paused);
|
||||
}
|
||||
if (pauseBtn) {
|
||||
pauseBtn.textContent = paused ? "Resume" : "Pause";
|
||||
}
|
||||
};
|
||||
|
||||
const setGoal = ({ label, progress, colors }) => {
|
||||
if (goalLabelEl) {
|
||||
goalLabelEl.innerHTML = "";
|
||||
if (Array.isArray(colors) && colors.length > 0) {
|
||||
colors.forEach((color) => {
|
||||
const swatch = document.createElement("span");
|
||||
swatch.style.background = color;
|
||||
swatch.style.display = "inline-block";
|
||||
swatch.style.width = "14px";
|
||||
swatch.style.height = "14px";
|
||||
swatch.style.borderRadius = "50%";
|
||||
swatch.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
swatch.style.marginRight = "6px";
|
||||
goalLabelEl.appendChild(swatch);
|
||||
});
|
||||
const text = document.createElement("span");
|
||||
text.textContent = label || "";
|
||||
goalLabelEl.appendChild(text);
|
||||
} else {
|
||||
goalLabelEl.textContent = label || "—";
|
||||
}
|
||||
}
|
||||
if (goalProgressEl)
|
||||
goalProgressEl.style.width = `${Math.max(
|
||||
0,
|
||||
Math.min(100, progress ?? 0),
|
||||
)}%`;
|
||||
};
|
||||
|
||||
const showWin = (message) => {
|
||||
if (winMessageEl) winMessageEl.textContent = message || "You win!";
|
||||
if (winEl) winEl.classList.add("visible");
|
||||
};
|
||||
|
||||
const hideWin = () => {
|
||||
if (winEl) winEl.classList.remove("visible");
|
||||
};
|
||||
|
||||
const showGameOver = (score) => {
|
||||
if (finalScoreEl) finalScoreEl.textContent = score;
|
||||
if (gameOverEl) gameOverEl.classList.add("visible");
|
||||
};
|
||||
|
||||
const hideGameOver = () => {
|
||||
if (gameOverEl) gameOverEl.classList.remove("visible");
|
||||
};
|
||||
|
||||
const spawnScorePopup = (point, amount, color) => {
|
||||
if (!point || !sceneEl) 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 updateHud = ({
|
||||
spawnIntervalMs,
|
||||
minChain,
|
||||
chainLength,
|
||||
score,
|
||||
highScore,
|
||||
activeColor,
|
||||
}) => {
|
||||
if (spawnRateEl) spawnRateEl.textContent = `${spawnIntervalMs} ms`;
|
||||
if (minLinkEl) minLinkEl.textContent = minChain;
|
||||
if (chainLenEl) chainLenEl.textContent = chainLength;
|
||||
if (scoreEl) scoreEl.textContent = score;
|
||||
if (highScoreEl) highScoreEl.textContent = highScore;
|
||||
if (activeColorEl) {
|
||||
if (activeColor) {
|
||||
activeColorEl.textContent = "";
|
||||
activeColorEl.style.display = "inline-block";
|
||||
activeColorEl.style.width = "14px";
|
||||
activeColorEl.style.height = "14px";
|
||||
activeColorEl.style.borderRadius = "50%";
|
||||
activeColorEl.style.background = activeColor;
|
||||
activeColorEl.style.border = "1px solid rgba(255,255,255,0.3)";
|
||||
} else {
|
||||
activeColorEl.removeAttribute("style");
|
||||
activeColorEl.textContent = "—";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildLegend = (palette) => {
|
||||
if (!paletteLegendEl) return;
|
||||
paletteLegendEl.innerHTML = "";
|
||||
palette.forEach((color) => {
|
||||
const swatch = document.createElement("span");
|
||||
swatch.style.background = color;
|
||||
paletteLegendEl.appendChild(swatch);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
sceneEl,
|
||||
updateHud,
|
||||
buildLegend,
|
||||
spawnScorePopup,
|
||||
setPauseState,
|
||||
showGameOver,
|
||||
hideGameOver,
|
||||
showWin,
|
||||
hideWin,
|
||||
setSceneOptions,
|
||||
setSceneSelection,
|
||||
setHandlers,
|
||||
setGoal,
|
||||
};
|
||||
};
|
||||
|
||||
window.PhysilinksUI = { create };
|
||||
})();
|
||||
Reference in New Issue
Block a user