308 lines
9.6 KiB
JavaScript
308 lines
9.6 KiB
JavaScript
(() => {
|
|
const create = () => {
|
|
const sceneEl = document.getElementById("scene-wrapper");
|
|
const floatingMessagesEl = document.getElementById("floating-messages");
|
|
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");
|
|
let messageDefaults = {
|
|
durationMs: 4200,
|
|
position: { xPercent: 50, yPercent: 12 },
|
|
};
|
|
|
|
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";
|
|
const sign = amount > 0 ? "+" : "";
|
|
el.textContent = `${sign}${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);
|
|
});
|
|
};
|
|
|
|
const setMessageDefaults = (overrides = {}) => {
|
|
messageDefaults = {
|
|
...messageDefaults,
|
|
...overrides,
|
|
position: {
|
|
...messageDefaults.position,
|
|
...(overrides.position || {}),
|
|
},
|
|
};
|
|
};
|
|
|
|
const renderFloatingMessage = (el, text, colors) => {
|
|
el.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";
|
|
el.appendChild(swatch);
|
|
});
|
|
}
|
|
const textSpan = document.createElement("span");
|
|
textSpan.textContent = text;
|
|
el.appendChild(textSpan);
|
|
};
|
|
|
|
const activeMessages = [];
|
|
|
|
const clearMessages = () => {
|
|
if (!floatingMessagesEl) return;
|
|
activeMessages.length = 0;
|
|
floatingMessagesEl.innerHTML = "";
|
|
};
|
|
|
|
const showFloatingMessage = (message, options = {}) => {
|
|
if (!floatingMessagesEl) return;
|
|
const msgObj =
|
|
typeof message === "string" ? { text: message } : message || {};
|
|
const text = (msgObj.text || "").trim();
|
|
if (!text) return;
|
|
const colors = Array.isArray(msgObj.colors) ? msgObj.colors : null;
|
|
const durationMs = Number.isFinite(options.durationMs)
|
|
? options.durationMs
|
|
: messageDefaults.durationMs;
|
|
const position = {
|
|
...messageDefaults.position,
|
|
...(options.position || {}),
|
|
};
|
|
const el = document.createElement("div");
|
|
el.className = "floating-message";
|
|
renderFloatingMessage(el, text, colors);
|
|
el.style.left = `${position.xPercent ?? 50}%`;
|
|
const verticalOffset = activeMessages.length * 34;
|
|
el.style.top = `calc(${position.yPercent ?? 10}% + ${verticalOffset}px)`;
|
|
floatingMessagesEl.appendChild(el);
|
|
requestAnimationFrame(() => {
|
|
el.classList.add("visible");
|
|
});
|
|
setTimeout(() => {
|
|
el.classList.remove("visible");
|
|
setTimeout(() => {
|
|
el.remove();
|
|
const idx = activeMessages.indexOf(el);
|
|
if (idx >= 0) activeMessages.splice(idx, 1);
|
|
}, 260);
|
|
}, durationMs);
|
|
activeMessages.push(el);
|
|
};
|
|
|
|
const api = {
|
|
sceneEl,
|
|
updateHud,
|
|
buildLegend,
|
|
spawnScorePopup,
|
|
setPauseState,
|
|
showGameOver,
|
|
hideGameOver,
|
|
showWin,
|
|
hideWin,
|
|
setSceneOptions,
|
|
setSceneSelection,
|
|
setHandlers,
|
|
setGoal,
|
|
showFloatingMessage,
|
|
setMessageDefaults,
|
|
clearMessages,
|
|
};
|
|
return api;
|
|
};
|
|
|
|
window.PhysilinksUI = {
|
|
create: (...args) => {
|
|
const instance = create(...args);
|
|
window.PhysilinksUI.instance = instance;
|
|
return instance;
|
|
},
|
|
instance: null,
|
|
};
|
|
})();
|