From 9243ac2df5ff6cdd30327076ef8ccd0d147a62ed Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Mon, 29 Dec 2025 16:01:30 +0100 Subject: [PATCH] Add new christmas level --- index.html | 1 + src/chain-controller.js | 13 ++- src/scenes/index.js | 1 + src/scenes/scene-christmas-calm.js | 171 +++++++++++++++++++++++++++++ src/spawn.js | 116 +++++++++++++++++++ 5 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/scenes/scene-christmas-calm.js diff --git a/index.html b/index.html index 810dc75..dc7d676 100644 --- a/index.html +++ b/index.html @@ -110,6 +110,7 @@ + diff --git a/src/chain-controller.js b/src/chain-controller.js index c28b115..afa964a 100644 --- a/src/chain-controller.js +++ b/src/chain-controller.js @@ -16,8 +16,17 @@ ui, }) => { const setHighlight = (body, on) => { - body.render.lineWidth = on ? 4 : 2; - body.render.strokeStyle = on ? "#f8fafc" : "#0b1222"; + const lineWidth = on ? 4 : 2; + 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 = () => { diff --git a/src/scenes/index.js b/src/scenes/index.js index eb5682d..929f4df 100644 --- a/src/scenes/index.js +++ b/src/scenes/index.js @@ -8,6 +8,7 @@ "scene-lava", "swirl-arena", "relax", + "christmas-calm", "stack-blocks-chaos", "low-g-terraces", "fast-drop-maze", diff --git a/src/scenes/scene-christmas-calm.js b/src/scenes/scene-christmas-calm.js new file mode 100644 index 0000000..e1566d2 --- /dev/null +++ b/src/scenes/scene-christmas-calm.js @@ -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)), + ]; + }, + }); +})(); diff --git a/src/spawn.js b/src/spawn.js index f5f0ea0..6d823d3 100644 --- a/src/spawn.js +++ b/src/spawn.js @@ -120,6 +120,7 @@ const createBallBodies = (x, y, color) => { const scene = getCurrentScene(); const ballPhysics = scene?.config?.ballPhysics || {}; + const debugSpawn = !!scene?.config?.debugSpawn; const commonOpts = { restitution: ballPhysics.restitution ?? 0.72, friction: ballPhysics.friction ?? 0.01, @@ -186,6 +187,71 @@ }; 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") { const side = config.ballRadius * 2; const body = Bodies.rectangle(x, y, side, side, { @@ -214,6 +280,12 @@ if (isGameOver()) return; const scene = getCurrentScene(); const sceneConfig = scene?.config || {}; + if (sceneConfig.debugSpawn) { + console.log("Spawn tick", { + sceneId: scene?.id, + ballShape: sceneConfig.ballShape, + }); + } const { width, height } = getDimensions(); const spawnLimit = sceneConfig.spawnLimit; if (Number.isFinite(spawnLimit) && spawnCount >= spawnLimit) { @@ -393,6 +465,50 @@ const scene = getCurrentScene(); const initialCount = scene?.config?.initialSpawnCount || 0; 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) { spawnBall(); }