Add new christmas level
This commit is contained in:
@@ -110,6 +110,7 @@
|
|||||||
<script src="./src/scenes/scene-fastdrop.js"></script>
|
<script src="./src/scenes/scene-fastdrop.js"></script>
|
||||||
<script src="./src/scenes/scene-lavalamp.js"></script>
|
<script src="./src/scenes/scene-lavalamp.js"></script>
|
||||||
<script src="./src/scenes/scene-relax.js"></script>
|
<script src="./src/scenes/scene-relax.js"></script>
|
||||||
|
<script src="./src/scenes/scene-christmas-calm.js"></script>
|
||||||
<script src="./src/scenes/scene-swirl-arena.js"></script>
|
<script src="./src/scenes/scene-swirl-arena.js"></script>
|
||||||
<script src="./src/scenes/scene-storm-grid.js"></script>
|
<script src="./src/scenes/scene-storm-grid.js"></script>
|
||||||
<script src="./src/scenes/scene-stack-blocks-chaos.js"></script>
|
<script src="./src/scenes/scene-stack-blocks-chaos.js"></script>
|
||||||
|
|||||||
@@ -16,8 +16,17 @@
|
|||||||
ui,
|
ui,
|
||||||
}) => {
|
}) => {
|
||||||
const setHighlight = (body, on) => {
|
const setHighlight = (body, on) => {
|
||||||
body.render.lineWidth = on ? 4 : 2;
|
const lineWidth = on ? 4 : 2;
|
||||||
body.render.strokeStyle = on ? "#f8fafc" : "#0b1222";
|
const strokeStyle = on ? "#f8fafc" : "#0b1222";
|
||||||
|
const applyHighlight = (target) => {
|
||||||
|
target.render = target.render || {};
|
||||||
|
target.render.lineWidth = lineWidth;
|
||||||
|
target.render.strokeStyle = strokeStyle;
|
||||||
|
};
|
||||||
|
applyHighlight(body);
|
||||||
|
if (Array.isArray(body.parts) && body.parts.length > 1) {
|
||||||
|
body.parts.forEach((part) => applyHighlight(part));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetChainVisuals = () => {
|
const resetChainVisuals = () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"scene-lava",
|
"scene-lava",
|
||||||
"swirl-arena",
|
"swirl-arena",
|
||||||
"relax",
|
"relax",
|
||||||
|
"christmas-calm",
|
||||||
"stack-blocks-chaos",
|
"stack-blocks-chaos",
|
||||||
"low-g-terraces",
|
"low-g-terraces",
|
||||||
"fast-drop-maze",
|
"fast-drop-maze",
|
||||||
|
|||||||
171
src/scenes/scene-christmas-calm.js
Normal file
171
src/scenes/scene-christmas-calm.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
(() => {
|
||||||
|
const { Bodies, Body, Vertices } = Matter;
|
||||||
|
const scenes = (window.PhysilinksSceneDefs =
|
||||||
|
window.PhysilinksSceneDefs || []);
|
||||||
|
|
||||||
|
scenes.push({
|
||||||
|
id: "christmas-calm",
|
||||||
|
name: "Christmas calm",
|
||||||
|
config: {
|
||||||
|
gravity: 0.5,
|
||||||
|
spawnIntervalMs: 720,
|
||||||
|
spawnBatchMin: 1,
|
||||||
|
spawnBatchMax: 2,
|
||||||
|
minChain: 3,
|
||||||
|
palette: ["#b91c1c", "#15803d", "#f59e0b", "#7dd3fc", "#fda4af"],
|
||||||
|
ballRadius: 20,
|
||||||
|
ballShape: "gift",
|
||||||
|
giftRibbonColor: "#f8fafc",
|
||||||
|
debugSpawn: true,
|
||||||
|
initialSpawnCount: 10,
|
||||||
|
initialSpawnArea: ({ width, height }) => ({
|
||||||
|
xMin: width * 0.28,
|
||||||
|
xMax: width * 0.72,
|
||||||
|
yMin: height * 0.76,
|
||||||
|
yMax: height * 0.88,
|
||||||
|
}),
|
||||||
|
winCondition: {
|
||||||
|
type: "score",
|
||||||
|
target: 12000,
|
||||||
|
onWin: { shoveBalls: true },
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
text: "Cozy winter glow",
|
||||||
|
position: { xPercent: 50, yPercent: 12 },
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
stiffness: 0.7,
|
||||||
|
lengthScale: 1.05,
|
||||||
|
damping: 0.05,
|
||||||
|
lineWidth: 4,
|
||||||
|
rope: true,
|
||||||
|
renderType: "line",
|
||||||
|
maxLengthMultiplier: 5.4,
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
colors: ["#0f172a", "#14532d", "#1f2937"],
|
||||||
|
opacity: 0.35,
|
||||||
|
blur: 26,
|
||||||
|
speedSec: 34,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createBodies: (w, h) => {
|
||||||
|
const wallThickness = Math.max(32, w * 0.045);
|
||||||
|
const wallHeight = h * 1.7;
|
||||||
|
const floorHeight = Math.max(70, h * 0.12);
|
||||||
|
const treeBaseRadius = Math.min(w, h) * 0.22;
|
||||||
|
const treeMidRadius = Math.min(w, h) * 0.16;
|
||||||
|
const treeTopRadius = Math.min(w, h) * 0.11;
|
||||||
|
const trunkWidth = treeBaseRadius * 0.3;
|
||||||
|
const trunkHeight = treeBaseRadius * 0.35;
|
||||||
|
const treeCenterX = w * 0.5;
|
||||||
|
const treeBaseY = h * 0.62;
|
||||||
|
const treeMidY = h * 0.52;
|
||||||
|
const treeTopY = h * 0.43;
|
||||||
|
const treeColor = "#166534";
|
||||||
|
const trunkColor = "#7c2d12";
|
||||||
|
|
||||||
|
const makeTriangle = (x, y, radius) => {
|
||||||
|
const points = [
|
||||||
|
{ x: 0, y: -radius },
|
||||||
|
{ x: radius, y: radius },
|
||||||
|
{ x: -radius, y: radius },
|
||||||
|
];
|
||||||
|
return Bodies.fromVertices(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
[points],
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
render: { fillStyle: treeColor, strokeStyle: treeColor },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeComet = (x, y, size) => {
|
||||||
|
const baseStar = 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 points = baseStar.map((p) => ({
|
||||||
|
x: (p.x - 50) * scale,
|
||||||
|
y: (p.y - 50) * scale,
|
||||||
|
}));
|
||||||
|
const star = Bodies.fromVertices(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
[points],
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
render: { fillStyle: "#facc15", strokeStyle: "#facc15" },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const tail = Bodies.rectangle(
|
||||||
|
x + size * 1.1,
|
||||||
|
y + size * 0.2,
|
||||||
|
size * 2.1,
|
||||||
|
size * 0.45,
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
angle: 0.35,
|
||||||
|
render: { fillStyle: "#fde68a", strokeStyle: "#fde68a" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const starParts =
|
||||||
|
Array.isArray(star.parts) && star.parts.length > 1
|
||||||
|
? star.parts
|
||||||
|
: [star];
|
||||||
|
return Body.create({
|
||||||
|
isStatic: true,
|
||||||
|
parts: [...starParts, tail],
|
||||||
|
render: { fillStyle: "#facc15", strokeStyle: "#facc15" },
|
||||||
|
plugin: { rotSpeed: 0.35 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
Bodies.rectangle(
|
||||||
|
w / 2,
|
||||||
|
h + floorHeight / 2,
|
||||||
|
w + wallThickness * 2,
|
||||||
|
floorHeight,
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
restitution: 0.7,
|
||||||
|
render: { fillStyle: "#e2e8f0", strokeStyle: "#e2e8f0" },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Bodies.rectangle(-wallThickness / 2, h / 2, wallThickness, wallHeight, {
|
||||||
|
isStatic: true,
|
||||||
|
render: { fillStyle: "#0f766e", strokeStyle: "#0f766e" },
|
||||||
|
}),
|
||||||
|
Bodies.rectangle(
|
||||||
|
w + wallThickness / 2,
|
||||||
|
h / 2,
|
||||||
|
wallThickness,
|
||||||
|
wallHeight,
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
render: { fillStyle: "#0f766e", strokeStyle: "#0f766e" },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
makeTriangle(treeCenterX, treeBaseY, treeBaseRadius),
|
||||||
|
makeTriangle(treeCenterX, treeMidY, treeMidRadius),
|
||||||
|
makeTriangle(treeCenterX, treeTopY, treeTopRadius),
|
||||||
|
Bodies.rectangle(
|
||||||
|
treeCenterX,
|
||||||
|
treeBaseY + treeBaseRadius * 0.65,
|
||||||
|
trunkWidth,
|
||||||
|
trunkHeight,
|
||||||
|
{
|
||||||
|
isStatic: true,
|
||||||
|
render: { fillStyle: trunkColor, strokeStyle: trunkColor },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
makeComet(w * 0.2, h * 0.25, Math.max(18, w * 0.025)),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
||||||
116
src/spawn.js
116
src/spawn.js
@@ -120,6 +120,7 @@
|
|||||||
const createBallBodies = (x, y, color) => {
|
const createBallBodies = (x, y, color) => {
|
||||||
const scene = getCurrentScene();
|
const scene = getCurrentScene();
|
||||||
const ballPhysics = scene?.config?.ballPhysics || {};
|
const ballPhysics = scene?.config?.ballPhysics || {};
|
||||||
|
const debugSpawn = !!scene?.config?.debugSpawn;
|
||||||
const commonOpts = {
|
const commonOpts = {
|
||||||
restitution: ballPhysics.restitution ?? 0.72,
|
restitution: ballPhysics.restitution ?? 0.72,
|
||||||
friction: ballPhysics.friction ?? 0.01,
|
friction: ballPhysics.friction ?? 0.01,
|
||||||
@@ -186,6 +187,71 @@
|
|||||||
};
|
};
|
||||||
return { bodies: [body], constraints: [], blobId: null };
|
return { bodies: [body], constraints: [], blobId: null };
|
||||||
}
|
}
|
||||||
|
if (scene?.config?.ballShape === "gift") {
|
||||||
|
const size = config.ballRadius * 2;
|
||||||
|
const ribbonSize = Math.max(4, config.ballRadius * 0.35);
|
||||||
|
const ribbonColor = scene?.config?.giftRibbonColor || "#f8fafc";
|
||||||
|
if (debugSpawn) {
|
||||||
|
console.log("Spawn gift", {
|
||||||
|
sceneId: scene?.id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const base = Bodies.rectangle(x, y, size, size, {
|
||||||
|
...commonOpts,
|
||||||
|
chamfer: { radius: Math.max(2, config.ballRadius * 0.18) },
|
||||||
|
});
|
||||||
|
const ribbonRender = {
|
||||||
|
fillStyle: ribbonColor,
|
||||||
|
strokeStyle: "#0b1222",
|
||||||
|
lineWidth: 2,
|
||||||
|
};
|
||||||
|
const verticalRibbon = Bodies.rectangle(x, y, ribbonSize, size * 1.02, {
|
||||||
|
render: ribbonRender,
|
||||||
|
});
|
||||||
|
const horizontalRibbon = Bodies.rectangle(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size * 1.02,
|
||||||
|
ribbonSize,
|
||||||
|
{
|
||||||
|
render: ribbonRender,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const bow = Bodies.rectangle(
|
||||||
|
x,
|
||||||
|
y - size * 0.34,
|
||||||
|
ribbonSize * 1.4,
|
||||||
|
ribbonSize * 0.8,
|
||||||
|
{
|
||||||
|
render: ribbonRender,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const body = Body.create({
|
||||||
|
parts: [base, verticalRibbon, horizontalRibbon, bow],
|
||||||
|
});
|
||||||
|
Body.setPosition(body, { x, y });
|
||||||
|
body.restitution = commonOpts.restitution;
|
||||||
|
body.friction = commonOpts.friction;
|
||||||
|
body.frictionAir = commonOpts.frictionAir;
|
||||||
|
body.frictionStatic = commonOpts.frictionStatic;
|
||||||
|
body.density = commonOpts.density;
|
||||||
|
body.render = {
|
||||||
|
...body.render,
|
||||||
|
...commonOpts.render,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
body.plugin = {
|
||||||
|
color,
|
||||||
|
hasEntered: false,
|
||||||
|
entryCheckId: null,
|
||||||
|
shape: "gift",
|
||||||
|
};
|
||||||
|
return { bodies: [body], constraints: [], blobId: null };
|
||||||
|
}
|
||||||
if (scene?.config?.ballShape === "rect") {
|
if (scene?.config?.ballShape === "rect") {
|
||||||
const side = config.ballRadius * 2;
|
const side = config.ballRadius * 2;
|
||||||
const body = Bodies.rectangle(x, y, side, side, {
|
const body = Bodies.rectangle(x, y, side, side, {
|
||||||
@@ -214,6 +280,12 @@
|
|||||||
if (isGameOver()) return;
|
if (isGameOver()) return;
|
||||||
const scene = getCurrentScene();
|
const scene = getCurrentScene();
|
||||||
const sceneConfig = scene?.config || {};
|
const sceneConfig = scene?.config || {};
|
||||||
|
if (sceneConfig.debugSpawn) {
|
||||||
|
console.log("Spawn tick", {
|
||||||
|
sceneId: scene?.id,
|
||||||
|
ballShape: sceneConfig.ballShape,
|
||||||
|
});
|
||||||
|
}
|
||||||
const { width, height } = getDimensions();
|
const { width, height } = getDimensions();
|
||||||
const spawnLimit = sceneConfig.spawnLimit;
|
const spawnLimit = sceneConfig.spawnLimit;
|
||||||
if (Number.isFinite(spawnLimit) && spawnCount >= spawnLimit) {
|
if (Number.isFinite(spawnLimit) && spawnCount >= spawnLimit) {
|
||||||
@@ -393,6 +465,50 @@
|
|||||||
const scene = getCurrentScene();
|
const scene = getCurrentScene();
|
||||||
const initialCount = scene?.config?.initialSpawnCount || 0;
|
const initialCount = scene?.config?.initialSpawnCount || 0;
|
||||||
if (!initialCount || initialCount <= 0) return;
|
if (!initialCount || initialCount <= 0) return;
|
||||||
|
if (scene?.config?.debugSpawn) {
|
||||||
|
console.log("Initial burst", {
|
||||||
|
sceneId: scene?.id,
|
||||||
|
initialCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const areaSource = scene?.config?.initialSpawnArea;
|
||||||
|
let area = null;
|
||||||
|
if (typeof areaSource === "function") {
|
||||||
|
try {
|
||||||
|
area = areaSource({ ...getDimensions(), world });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("initialSpawnArea function failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scene?.config?.debugSpawn) {
|
||||||
|
console.log("Initial spawn area", {
|
||||||
|
sceneId: scene?.id,
|
||||||
|
area,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (area && Number.isFinite(area.xMin) && Number.isFinite(area.xMax)) {
|
||||||
|
const { width, height } = getDimensions();
|
||||||
|
const pad = config.ballRadius + 4;
|
||||||
|
const minX = Math.max(pad, Math.min(area.xMin, area.xMax));
|
||||||
|
const maxX = Math.min(width - pad, Math.max(area.xMin, area.xMax));
|
||||||
|
const minY = Math.max(pad, Math.min(area.yMin, area.yMax));
|
||||||
|
const maxY = Math.min(height - pad, Math.max(area.yMin, area.yMax));
|
||||||
|
if (scene?.config?.debugSpawn) {
|
||||||
|
console.log("Initial spawn bounds", {
|
||||||
|
sceneId: scene?.id,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < initialCount; i += 1) {
|
||||||
|
const x = minX + Math.random() * Math.max(0, maxX - minX);
|
||||||
|
const y = minY + Math.random() * Math.max(0, maxY - minY);
|
||||||
|
spawnAtPosition({ x, y, markEntered: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (let i = 0; i < initialCount; i += 1) {
|
for (let i = 0; i < initialCount; i += 1) {
|
||||||
spawnBall();
|
spawnBall();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user