diff --git a/README.md b/README.md
index e8ea673..3988666 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Physilinks is a browser-based physics linking game built with Matter.js. Match a
- `styles.css`: Styling for canvas, HUD, overlays, and score popups.
- `src/scenes/`: Scene presets split per file (`scene-*.js`) plus `index.js` that registers them to `window.PhysilinksScenes` (e.g., zero-G grid, balanced, low-G, fast drop, lava drift).
- `src/scenes/scene-template.js`: Reference-only template documenting every scene config option; not loaded by default.
+- `src/config.js`: Base game config defaults (gravity, spawn timing, link settings, palettes, message defaults).
- `src/decomp-setup.js`: Registers `poly-decomp` with Matter to allow concave shapes (stars, blobs) built via `Bodies.fromVertices`.
- `src/ui.js`: DOM access, HUD updates, overlays, popups, and control/selector wiring.
- `src/spawn.js`: Spawner utilities (intervals, batch/column/grid spawns), ball creation (shapes/blobs), radius scaling, and blob cleanup.
diff --git a/index.html b/index.html
index 775de82..074299f 100644
--- a/index.html
+++ b/index.html
@@ -105,6 +105,7 @@
+
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..0d181f5
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,37 @@
+(() => {
+ const create = () => {
+ const defaultMessageConfig = {
+ durationMs: 4200,
+ position: { xPercent: 50, yPercent: 10 },
+ text: null,
+ colors: null,
+ };
+
+ const config = {
+ gravity: 1,
+ spawnIntervalMs: 520,
+ autoSpawn: true,
+ minChain: 3,
+ palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
+ ballRadius: 18,
+ ballShape: "circle",
+ link: {
+ stiffness: 0.85,
+ lengthScale: 1.05, // max stretch factor; slack below this
+ damping: 0.08,
+ lineWidth: 3,
+ rope: true,
+ renderType: "line",
+ maxLengthMultiplier: 3.1,
+ },
+ messages: { ...defaultMessageConfig },
+ };
+
+ return {
+ defaultMessageConfig: { ...defaultMessageConfig },
+ config: { ...config, link: { ...config.link }, messages: { ...config.messages } },
+ };
+ };
+
+ window.PhysilinksConfig = { create };
+})();
diff --git a/src/main.js b/src/main.js
index 45c84bf..f7c7304 100644
--- a/src/main.js
+++ b/src/main.js
@@ -2,6 +2,24 @@
const { Engine, Render, Runner, World, Body, Constraint, Events, Vector } =
Matter;
+ const { config: baseConfig, defaultMessageConfig } = window.PhysilinksConfig
+ ?.create
+ ? window.PhysilinksConfig.create()
+ : {
+ config: {},
+ defaultMessageConfig: {
+ durationMs: 4200,
+ position: { xPercent: 50, yPercent: 10 },
+ text: null,
+ colors: null,
+ },
+ };
+ const config = {
+ ...baseConfig,
+ link: { ...(baseConfig?.link || {}) },
+ messages: { ...(baseConfig?.messages || {}) },
+ };
+
const {
scenes = [],
defaultSceneId,
@@ -10,33 +28,6 @@
const { getSceneById, getSceneIdFromUrl, setSceneIdInUrl, getNextSceneId } =
window.PhysilinksSceneRegistry || {};
- const defaultMessageConfig = {
- durationMs: 4200,
- position: { xPercent: 50, yPercent: 10 },
- text: null,
- colors: null,
- };
-
- const config = {
- gravity: 1,
- spawnIntervalMs: 520,
- autoSpawn: true,
- minChain: 3,
- palette: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"],
- ballRadius: 18,
- ballShape: "circle",
- link: {
- stiffness: 0.85,
- lengthScale: 1.05, // max stretch factor; slack below this
- damping: 0.08,
- lineWidth: 3,
- rope: true,
- renderType: "line",
- maxLengthMultiplier: 3.1,
- },
- messages: { ...defaultMessageConfig },
- };
-
const ui = window.PhysilinksUI.create();
const { sceneEl } = ui;
@@ -87,10 +78,13 @@
let rotators = [];
let oscillators = [];
const initialSceneId =
- getSceneIdFromUrl() ||
- (getSceneById(defaultSceneId) ? defaultSceneId : null) ||
+ (getSceneIdFromUrl && getSceneIdFromUrl(scenes)) ||
+ (getSceneById && getSceneById(scenes, defaultSceneId)
+ ? defaultSceneId
+ : null) ||
scenes[0]?.id;
- let currentScene = getSceneById(initialSceneId) || scenes[0] || null;
+ let currentScene =
+ (getSceneById && getSceneById(scenes, initialSceneId)) || scenes[0] || null;
if (currentScene && currentScene.config) {
Object.assign(config, currentScene.config);
@@ -128,7 +122,7 @@
} = window.PhysilinksStorage || {};
const applyScene = (sceneId) => {
- const next = getSceneById(sceneId) || scenes[0];
+ const next = (getSceneById && getSceneById(scenes, sceneId)) || scenes[0];
if (!next) return;
currentScene = next;
ui.setSceneSelection(next.id);
diff --git a/src/scene-registry.js b/src/scene-registry.js
index 14c4f74..35c48d4 100644
--- a/src/scene-registry.js
+++ b/src/scene-registry.js
@@ -1,8 +1,11 @@
(() => {
- const getSceneById = (scenes, sceneId) =>
- scenes.find((candidate) => candidate.id === sceneId) || null;
+ const getSceneById = (scenes, sceneId) => {
+ if (!Array.isArray(scenes)) return null;
+ return scenes.find((candidate) => candidate.id === sceneId) || null;
+ };
const getSceneIdFromUrl = (scenes) => {
+ if (!Array.isArray(scenes)) return null;
try {
const params = new URLSearchParams(window.location.search);
const urlScene = params.get("scene");
@@ -24,8 +27,10 @@
};
const getNextSceneId = (scenes, order, currentScene) => {
+ if (!Array.isArray(scenes)) return null;
+ const ordered = Array.isArray(order) ? order : [];
const currentId = currentScene?.id;
- const orderedExisting = order.filter((id) =>
+ const orderedExisting = ordered.filter((id) =>
scenes.some((s) => s.id === id),
);
if (orderedExisting.length > 0 && currentId) {