Files
Physilinks/src/ui.js

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,
};
})();