diff --git a/src/main.js b/src/main.js index 5e395cb..051d94e 100644 --- a/src/main.js +++ b/src/main.js @@ -1,43 +1,89 @@ (() => { const { 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 fromWindow = (key, fallback = {}) => window[key] ?? fallback; - const config = { - ...baseConfig, - link: { ...(baseConfig?.link || {}) }, - messages: { ...(baseConfig?.messages || {}) }, + const getFactory = (key, fallbackFactory) => { + const mod = fromWindow(key); + return typeof mod.create === "function" ? mod.create : fallbackFactory; + }; + + const createConfig = getFactory("PhysilinksConfig", () => ({ + config: {}, + defaultMessageConfig: { + durationMs: 4200, + position: { xPercent: 50, yPercent: 10 }, + text: null, + colors: null, + }, + })); + + const { config: baseConfig, defaultMessageConfig } = createConfig(); + + const normalizeMessages = ( + sceneMessages = {}, + defaults = defaultMessageConfig, + ) => ({ + durationMs: Number.isFinite(sceneMessages.durationMs) + ? sceneMessages.durationMs + : defaults.durationMs, + text: + typeof sceneMessages.text === "string" && sceneMessages.text.trim() + ? sceneMessages.text.trim() + : defaults.text, + position: { + xPercent: + typeof sceneMessages.position?.xPercent === "number" + ? sceneMessages.position.xPercent + : defaults.position.xPercent, + yPercent: + typeof sceneMessages.position?.yPercent === "number" + ? sceneMessages.position.yPercent + : defaults.position.yPercent, + }, + colors: Array.isArray(sceneMessages.colors) + ? sceneMessages.colors + : defaults.colors, + }); + + const normalizeSceneConfig = (sceneConfig = {}) => { + const { link = {}, messages = {}, ...rest } = sceneConfig; + return { + ...rest, + link: { ...link }, + messages: normalizeMessages(messages), + }; + }; + + const config = { ...normalizeSceneConfig(baseConfig) }; + + const setConfigForScene = (sceneConfig = {}) => { + const normalized = normalizeSceneConfig(sceneConfig); + Object.assign(config, normalized); + config.link = { ...normalized.link }; + config.messages = normalized.messages; }; const { scenes = [], defaultSceneId, order: sceneOrder = [], - } = window.PhysilinksScenes || {}; + } = fromWindow("PhysilinksScenes", {}); const { getSceneById, getSceneIdFromUrl, setSceneIdInUrl, getNextSceneId } = - window.PhysilinksSceneRegistry || {}; + fromWindow("PhysilinksSceneRegistry", {}); - const ui = window.PhysilinksUI.create(); + const createUI = getFactory("PhysilinksUI"); + const ui = createUI(); const { sceneEl } = ui; let width = sceneEl.clientWidth; let height = sceneEl.clientHeight; const BALL_BASELINE = 680; // reference height used for relative ball sizing + const createEngine = getFactory("PhysilinksEngine"); const { engine, render, runner, startRunner, stopRunner, setRenderSize } = - window.PhysilinksEngine.create({ + createEngine({ sceneEl, width, height, @@ -64,13 +110,14 @@ (getSceneById && getSceneById(scenes, initialSceneId)) || scenes[0] || null; if (currentScene && currentScene.config) { - Object.assign(config, currentScene.config); - config.link = { ...currentScene.config.link }; + setConfigForScene(currentScene.config); } const rebuildSceneBodies = () => { boundaries.forEach((b) => World.remove(world, b)); - boundaries = currentScene.createBodies(width, height); + const nextBoundaries = currentScene.createBodies(width, height); + boundaries.length = 0; + boundaries.push(...nextBoundaries); rotators = boundaries.filter((b) => b.plugin && b.plugin.rotSpeed); oscillators = boundaries.filter((b) => b.plugin && b.plugin.oscillate); World.add(world, boundaries); @@ -91,12 +138,41 @@ let spawnSystem = null; let input = null; + const storage = fromWindow("PhysilinksStorage", {}); const { loadHighScore = () => 0, loadLongestChain = () => 0, saveHighScore = () => {}, saveLongestChain = () => {}, - } = window.PhysilinksStorage || {}; + } = storage; + + const resetEngineForScene = ( + sceneConfig, + { prevRadius, timeScaleOverride, resetPlugins = true } = {}, + ) => { + if (resetPlugins) { + world.plugin = world.plugin || {}; + world.plugin.stormSteps = Array.isArray(sceneConfig?.spawnIntervals) + ? sceneConfig.spawnIntervals + : null; + world.plugin.squareOffset = 0; + engine.plugin = engine.plugin || {}; + engine.plugin.stormState = null; + } + spawnSystem.updateBallRadius(prevRadius); + engine.gravity.scale = + typeof sceneConfig?.gravityScale === "number" + ? sceneConfig.gravityScale + : defaultGravityScale; + engine.gravity.x = 0; + engine.gravity.y = config.gravity; + engine.timing.timeScale = + typeof timeScaleOverride === "number" + ? timeScaleOverride + : typeof sceneConfig?.timeScale === "number" + ? sceneConfig.timeScale + : defaultTimeScale; + }; const applyScene = (sceneId) => { const next = (getSceneById && getSceneById(scenes, sceneId)) || scenes[0]; @@ -105,50 +181,9 @@ ui.setSceneSelection(next.id); setSceneIdInUrl(next.id); const prevRadius = config.ballRadius; - Object.assign(config, next.config); - config.link = { ...(next.config?.link || {}) }; - const sceneMessages = next.config?.messages || {}; - config.messages = { - durationMs: Number.isFinite(sceneMessages.durationMs) - ? sceneMessages.durationMs - : defaultMessageConfig.durationMs, - text: - typeof sceneMessages.text === "string" && sceneMessages.text.trim() - ? sceneMessages.text.trim() - : defaultMessageConfig.text, - position: { - xPercent: - typeof sceneMessages.position?.xPercent === "number" - ? sceneMessages.position.xPercent - : defaultMessageConfig.position.xPercent, - yPercent: - typeof sceneMessages.position?.yPercent === "number" - ? sceneMessages.position.yPercent - : defaultMessageConfig.position.yPercent, - }, - colors: Array.isArray(sceneMessages.colors) - ? sceneMessages.colors - : defaultMessageConfig.colors, - }; + setConfigForScene(next.config); ui.setMessageDefaults(config.messages); - world.plugin = world.plugin || {}; - world.plugin.stormSteps = Array.isArray(next.config?.spawnIntervals) - ? next.config.spawnIntervals - : null; - world.plugin.squareOffset = 0; - engine.plugin = engine.plugin || {}; - engine.plugin.stormState = null; - engine.gravity.scale = - typeof next.config.gravityScale === "number" - ? next.config.gravityScale - : defaultGravityScale; - engine.timing.timeScale = - typeof next.config.timeScale === "number" - ? next.config.timeScale - : defaultTimeScale; - spawnSystem.updateBallRadius(prevRadius); - engine.gravity.x = 0; - engine.gravity.y = config.gravity; + resetEngineForScene(next.config, { prevRadius }); clearedCount = 0; levelWon = false; clearedByColor = {}; @@ -193,7 +228,8 @@ ui.showGameOver(score); }; - spawnSystem = window.PhysilinksSpawn.create({ + const createSpawn = getFactory("PhysilinksSpawn"); + spawnSystem = createSpawn({ config, world, balls, @@ -205,7 +241,8 @@ isGameOver: () => gameOver, ballBaseline: BALL_BASELINE, }); - const goals = window.PhysilinksGoals.create({ + const createGoals = getFactory("PhysilinksGoals"); + const goals = createGoals({ config, getCurrentScene: () => currentScene, getScore: () => score, @@ -245,13 +282,10 @@ ui.hideGameOver(); ui.hideWin(); ui.setPauseState(false); - engine.gravity.scale = - typeof currentScene?.config?.gravityScale === "number" - ? currentScene.config.gravityScale - : defaultGravityScale; - engine.gravity.x = 0; - engine.gravity.y = config.gravity; - engine.timing.timeScale = 1; + resetEngineForScene(currentScene?.config || {}, { + timeScaleOverride: 1, + resetPlugins: false, + }); startRunner(); updateHud(); if (isGridScene()) { @@ -341,7 +375,10 @@ remaining.push(b); } }); - boundaries = remaining; + boundaries.length = 0; + boundaries.push(...remaining); + rotators = rotators.filter((b) => boundaries.includes(b)); + oscillators = oscillators.filter((b) => boundaries.includes(b)); } }; @@ -508,7 +545,8 @@ goals.maybeAnnounceGoalProgress(goal); }; - input = window.PhysilinksInput.create({ + const createInput = getFactory("PhysilinksInput"); + input = createInput({ render, world, balls,