460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
const TABLET_META = {
|
||
union: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
|
||
spirit: { label: "Enochian Tablet of Union", element: "Spirit", order: 0 },
|
||
earth: { label: "Enochian Tablet of Earth", element: "Earth", order: 1 },
|
||
air: { label: "Enochian Tablet of Air", element: "Air", order: 2 },
|
||
water: { label: "Enochian Tablet of Water", element: "Water", order: 3 },
|
||
fire: { label: "Enochian Tablet of Fire", element: "Fire", order: 4 }
|
||
};
|
||
|
||
const TAROT_NAME_ALIASES = {
|
||
juggler: "magus",
|
||
magician: "magus",
|
||
strength: "lust",
|
||
temperance: "art",
|
||
judgement: "aeon",
|
||
judgment: "aeon",
|
||
charit: "chariot"
|
||
};
|
||
|
||
const state = {
|
||
initialized: false,
|
||
entries: [],
|
||
filteredEntries: [],
|
||
selectedId: "",
|
||
selectedCell: null,
|
||
searchQuery: "",
|
||
lettersById: new Map()
|
||
};
|
||
|
||
function getElements() {
|
||
return {
|
||
listEl: document.getElementById("enochian-list"),
|
||
countEl: document.getElementById("enochian-count"),
|
||
searchEl: document.getElementById("enochian-search-input"),
|
||
searchClearEl: document.getElementById("enochian-search-clear"),
|
||
detailNameEl: document.getElementById("enochian-detail-name"),
|
||
detailSubEl: document.getElementById("enochian-detail-sub"),
|
||
detailBodyEl: document.getElementById("enochian-detail-body")
|
||
};
|
||
}
|
||
|
||
function normalize(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
function titleCase(value) {
|
||
return String(value || "")
|
||
.split(" ")
|
||
.filter(Boolean)
|
||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||
.join(" ");
|
||
}
|
||
|
||
function getTabletMeta(id) {
|
||
const normalizedId = normalize(id);
|
||
return TABLET_META[normalizedId] || {
|
||
label: `Enochian Tablet of ${titleCase(normalizedId || "Unknown")}`,
|
||
element: titleCase(normalizedId || "Unknown"),
|
||
order: 99
|
||
};
|
||
}
|
||
|
||
function buildSearchText(entry) {
|
||
return normalize([
|
||
entry.id,
|
||
entry.label,
|
||
entry.element,
|
||
entry.rowCount,
|
||
entry.colCount,
|
||
...entry.uniqueLetters
|
||
].join(" "));
|
||
}
|
||
|
||
function buildEntries(magickDataset) {
|
||
const tablets = magickDataset?.grouped?.enochian?.tablets;
|
||
if (!tablets || typeof tablets !== "object") {
|
||
return [];
|
||
}
|
||
|
||
return Object.entries(tablets)
|
||
.map(([key, value]) => {
|
||
const id = normalize(value?.id || key);
|
||
const grid = Array.isArray(value?.grid)
|
||
? value.grid.map((row) => (Array.isArray(row)
|
||
? row.map((cell) => String(cell || "").trim())
|
||
: []))
|
||
: [];
|
||
|
||
const rowCount = grid.length;
|
||
const colCount = grid.reduce((max, row) => Math.max(max, row.length), 0);
|
||
const uniqueLetters = [...new Set(
|
||
grid
|
||
.flat()
|
||
.map((cell) => String(cell || "").trim().toUpperCase())
|
||
.filter(Boolean)
|
||
)].sort((left, right) => left.localeCompare(right));
|
||
|
||
const meta = getTabletMeta(id);
|
||
|
||
return {
|
||
id,
|
||
grid,
|
||
rowCount,
|
||
colCount,
|
||
uniqueLetters,
|
||
element: meta.element,
|
||
label: meta.label,
|
||
order: Number(meta.order)
|
||
};
|
||
})
|
||
.sort((left, right) => left.order - right.order || left.label.localeCompare(right.label));
|
||
}
|
||
|
||
function buildLetterMap(magickDataset) {
|
||
const letters = magickDataset?.grouped?.enochian?.letters;
|
||
if (!letters || typeof letters !== "object") {
|
||
return new Map();
|
||
}
|
||
|
||
return new Map(
|
||
Object.entries(letters)
|
||
.map(([key, value]) => [String(key || "").trim().toUpperCase(), value])
|
||
.filter(([key]) => Boolean(key))
|
||
);
|
||
}
|
||
|
||
function findEntryById(id) {
|
||
return state.entries.find((entry) => entry.id === id) || null;
|
||
}
|
||
|
||
function getDefaultCell(entry) {
|
||
if (!entry || !Array.isArray(entry.grid)) {
|
||
return null;
|
||
}
|
||
|
||
for (let rowIndex = 0; rowIndex < entry.grid.length; rowIndex += 1) {
|
||
const row = entry.grid[rowIndex];
|
||
for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
|
||
const value = String(row[colIndex] || "").trim();
|
||
if (value) {
|
||
return { rowIndex, colIndex, value };
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function renderList(elements) {
|
||
if (!elements?.listEl) {
|
||
return;
|
||
}
|
||
|
||
elements.listEl.replaceChildren();
|
||
|
||
state.filteredEntries.forEach((entry) => {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "enoch-list-item";
|
||
button.dataset.tabletId = entry.id;
|
||
button.setAttribute("role", "option");
|
||
|
||
const isSelected = entry.id === state.selectedId;
|
||
button.classList.toggle("is-selected", isSelected);
|
||
button.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "enoch-list-name";
|
||
name.textContent = entry.label;
|
||
|
||
const meta = document.createElement("span");
|
||
meta.className = "enoch-list-meta";
|
||
meta.textContent = `${entry.element} · ${entry.rowCount}×${entry.colCount} · ${entry.uniqueLetters.length} letters`;
|
||
|
||
button.append(name, meta);
|
||
elements.listEl.appendChild(button);
|
||
});
|
||
|
||
if (elements.countEl) {
|
||
elements.countEl.textContent = `${state.filteredEntries.length} tablets`;
|
||
}
|
||
|
||
if (!state.filteredEntries.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "planet-text";
|
||
empty.style.padding = "16px";
|
||
empty.style.color = "#71717a";
|
||
empty.textContent = "No Enochian tablets match your search.";
|
||
elements.listEl.appendChild(empty);
|
||
}
|
||
}
|
||
|
||
function resolveTarotCardName(value) {
|
||
const normalized = normalize(value);
|
||
if (!normalized) {
|
||
return "";
|
||
}
|
||
|
||
return TAROT_NAME_ALIASES[normalized] || normalized;
|
||
}
|
||
|
||
function renderDetail(elements) {
|
||
if (!elements?.detailBodyEl || !elements.detailNameEl || !elements.detailSubEl) {
|
||
return;
|
||
}
|
||
|
||
const entry = findEntryById(state.selectedId);
|
||
elements.detailBodyEl.replaceChildren();
|
||
|
||
if (!entry) {
|
||
elements.detailNameEl.textContent = "--";
|
||
elements.detailSubEl.textContent = "Select a tablet to explore";
|
||
return;
|
||
}
|
||
|
||
elements.detailNameEl.textContent = entry.label;
|
||
elements.detailSubEl.textContent = `${entry.element} Tablet · ${entry.rowCount} rows × ${entry.colCount} columns`;
|
||
|
||
if (!state.selectedCell) {
|
||
state.selectedCell = getDefaultCell(entry);
|
||
}
|
||
|
||
const detailGrid = document.createElement("div");
|
||
detailGrid.className = "planet-meta-grid";
|
||
|
||
const summaryCard = document.createElement("div");
|
||
summaryCard.className = "planet-meta-card";
|
||
summaryCard.innerHTML = `
|
||
<strong>Tablet Overview</strong>
|
||
<div class="enoch-letter-meta">
|
||
<div>${entry.label}</div>
|
||
<div>${entry.rowCount} rows × ${entry.colCount} columns</div>
|
||
<div>Unique letters: ${entry.uniqueLetters.length}</div>
|
||
</div>
|
||
`;
|
||
|
||
const gridCard = document.createElement("div");
|
||
gridCard.className = "planet-meta-card";
|
||
const gridTitle = document.createElement("strong");
|
||
gridTitle.textContent = "Tablet Grid";
|
||
const gridEl = document.createElement("div");
|
||
gridEl.className = "enoch-grid";
|
||
|
||
entry.grid.forEach((row, rowIndex) => {
|
||
const rowEl = document.createElement("div");
|
||
rowEl.className = "enoch-grid-row";
|
||
row.forEach((cell, colIndex) => {
|
||
const value = String(cell || "").trim();
|
||
const cellBtn = document.createElement("button");
|
||
cellBtn.type = "button";
|
||
cellBtn.className = "enoch-grid-cell";
|
||
cellBtn.textContent = value || "·";
|
||
|
||
const isSelectedCell = state.selectedCell
|
||
&& state.selectedCell.rowIndex === rowIndex
|
||
&& state.selectedCell.colIndex === colIndex;
|
||
cellBtn.classList.toggle("is-selected", Boolean(isSelectedCell));
|
||
|
||
cellBtn.addEventListener("click", () => {
|
||
state.selectedCell = { rowIndex, colIndex, value };
|
||
renderDetail(elements);
|
||
});
|
||
|
||
rowEl.appendChild(cellBtn);
|
||
});
|
||
gridEl.appendChild(rowEl);
|
||
});
|
||
|
||
gridCard.append(gridTitle, gridEl);
|
||
|
||
const letterCard = document.createElement("div");
|
||
letterCard.className = "planet-meta-card";
|
||
const letterTitle = document.createElement("strong");
|
||
letterTitle.textContent = "Selected Letter";
|
||
|
||
const selectedLetter = String(state.selectedCell?.value || "").trim().toUpperCase();
|
||
const letterData = selectedLetter ? state.lettersById.get(selectedLetter) : null;
|
||
|
||
const letterContent = document.createElement("div");
|
||
letterContent.className = "enoch-letter-meta";
|
||
|
||
if (!selectedLetter) {
|
||
letterContent.textContent = "Select any grid cell to inspect its correspondence data.";
|
||
} else {
|
||
const firstRow = document.createElement("div");
|
||
firstRow.className = "enoch-letter-row";
|
||
const chip = document.createElement("span");
|
||
chip.className = "enoch-letter-chip";
|
||
chip.textContent = selectedLetter;
|
||
firstRow.appendChild(chip);
|
||
|
||
const title = document.createElement("span");
|
||
title.textContent = letterData?.title
|
||
? `${letterData.title}${letterData.english ? ` · ${letterData.english}` : ""}`
|
||
: "No letter metadata yet";
|
||
firstRow.appendChild(title);
|
||
letterContent.appendChild(firstRow);
|
||
|
||
if (letterData) {
|
||
const detailRows = [
|
||
["Pronunciation", letterData.pronounciation],
|
||
["Planet / Element", letterData["planet/element"]],
|
||
["Tarot", letterData.tarot],
|
||
["Gematria", letterData.gematria]
|
||
];
|
||
|
||
detailRows.forEach(([label, value]) => {
|
||
if (value === undefined || value === null || String(value).trim() === "") {
|
||
return;
|
||
}
|
||
const row = document.createElement("div");
|
||
row.className = "enoch-letter-row";
|
||
row.innerHTML = `<span style="color:#a1a1aa">${label}:</span><span>${value}</span>`;
|
||
letterContent.appendChild(row);
|
||
});
|
||
|
||
const navRow = document.createElement("div");
|
||
navRow.className = "enoch-letter-row";
|
||
|
||
const tarotCardName = resolveTarotCardName(letterData.tarot);
|
||
if (tarotCardName) {
|
||
const tarotBtn = document.createElement("button");
|
||
tarotBtn.type = "button";
|
||
tarotBtn.className = "enoch-nav-btn";
|
||
tarotBtn.textContent = `Open Tarot (${titleCase(tarotCardName)}) ↗`;
|
||
tarotBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:tarot-trump", {
|
||
detail: { cardName: tarotCardName }
|
||
}));
|
||
});
|
||
navRow.appendChild(tarotBtn);
|
||
}
|
||
|
||
const alphabetBtn = document.createElement("button");
|
||
alphabetBtn.type = "button";
|
||
alphabetBtn.className = "enoch-nav-btn";
|
||
alphabetBtn.textContent = "Open Alphabet ↗";
|
||
alphabetBtn.addEventListener("click", () => {
|
||
document.dispatchEvent(new CustomEvent("nav:alphabet", {
|
||
detail: {
|
||
alphabet: "english",
|
||
englishLetter: selectedLetter
|
||
}
|
||
}));
|
||
});
|
||
navRow.appendChild(alphabetBtn);
|
||
|
||
letterContent.appendChild(navRow);
|
||
}
|
||
}
|
||
|
||
letterCard.append(letterTitle, letterContent);
|
||
|
||
detailGrid.append(summaryCard, letterCard, gridCard);
|
||
elements.detailBodyEl.appendChild(detailGrid);
|
||
}
|
||
|
||
function applyFilter(elements) {
|
||
const query = normalize(state.searchQuery);
|
||
state.filteredEntries = query
|
||
? state.entries.filter((entry) => buildSearchText(entry).includes(query))
|
||
: [...state.entries];
|
||
|
||
if (elements?.searchClearEl) {
|
||
elements.searchClearEl.disabled = !query;
|
||
}
|
||
|
||
if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) {
|
||
state.selectedId = state.filteredEntries[0]?.id || "";
|
||
state.selectedCell = state.selectedId ? getDefaultCell(findEntryById(state.selectedId)) : null;
|
||
}
|
||
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
}
|
||
|
||
function selectByTabletId(tabletId) {
|
||
const elements = getElements();
|
||
const target = findEntryById(normalize(tabletId));
|
||
if (!target) {
|
||
return false;
|
||
}
|
||
|
||
state.selectedId = target.id;
|
||
state.selectedCell = getDefaultCell(target);
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
return true;
|
||
}
|
||
|
||
function ensureEnochianSection(magickDataset) {
|
||
const elements = getElements();
|
||
if (!elements.listEl || !elements.detailBodyEl) {
|
||
return;
|
||
}
|
||
|
||
state.entries = buildEntries(magickDataset);
|
||
state.lettersById = buildLetterMap(magickDataset);
|
||
|
||
if (!state.selectedId && state.entries.length) {
|
||
state.selectedId = state.entries[0].id;
|
||
state.selectedCell = getDefaultCell(state.entries[0]);
|
||
}
|
||
|
||
applyFilter(elements);
|
||
|
||
if (state.initialized) {
|
||
return;
|
||
}
|
||
|
||
elements.listEl.addEventListener("click", (event) => {
|
||
const target = event.target instanceof Element
|
||
? event.target.closest(".enoch-list-item")
|
||
: null;
|
||
if (!(target instanceof HTMLButtonElement)) {
|
||
return;
|
||
}
|
||
|
||
const tabletId = target.dataset.tabletId;
|
||
if (!tabletId) {
|
||
return;
|
||
}
|
||
|
||
state.selectedId = tabletId;
|
||
state.selectedCell = getDefaultCell(findEntryById(tabletId));
|
||
renderList(elements);
|
||
renderDetail(elements);
|
||
});
|
||
|
||
if (elements.searchEl) {
|
||
elements.searchEl.addEventListener("input", () => {
|
||
state.searchQuery = elements.searchEl.value || "";
|
||
applyFilter(elements);
|
||
});
|
||
}
|
||
|
||
if (elements.searchClearEl && elements.searchEl) {
|
||
elements.searchClearEl.addEventListener("click", () => {
|
||
state.searchQuery = "";
|
||
elements.searchEl.value = "";
|
||
applyFilter(elements);
|
||
elements.searchEl.focus();
|
||
});
|
||
}
|
||
|
||
state.initialized = true;
|
||
}
|
||
|
||
window.EnochianSectionUi = {
|
||
ensureEnochianSection,
|
||
selectByTabletId
|
||
};
|
||
})();
|