819 lines
24 KiB
JavaScript
819 lines
24 KiB
JavaScript
/* ui-tarot-relations.js — Tarot relation builders */
|
|
(function () {
|
|
"use strict";
|
|
|
|
const TAROT_TRUMP_NUMBER_BY_NAME = {
|
|
"the fool": 0,
|
|
fool: 0,
|
|
"the magus": 1,
|
|
magus: 1,
|
|
magician: 1,
|
|
"the high priestess": 2,
|
|
"high priestess": 2,
|
|
"the empress": 3,
|
|
empress: 3,
|
|
"the emperor": 4,
|
|
emperor: 4,
|
|
"the hierophant": 5,
|
|
hierophant: 5,
|
|
"the lovers": 6,
|
|
lovers: 6,
|
|
"the chariot": 7,
|
|
chariot: 7,
|
|
strength: 8,
|
|
lust: 8,
|
|
"the hermit": 9,
|
|
hermit: 9,
|
|
fortune: 10,
|
|
"wheel of fortune": 10,
|
|
justice: 11,
|
|
"the hanged man": 12,
|
|
"hanged man": 12,
|
|
death: 13,
|
|
temperance: 14,
|
|
art: 14,
|
|
"the devil": 15,
|
|
devil: 15,
|
|
"the tower": 16,
|
|
tower: 16,
|
|
"the star": 17,
|
|
star: 17,
|
|
"the moon": 18,
|
|
moon: 18,
|
|
"the sun": 19,
|
|
sun: 19,
|
|
aeon: 20,
|
|
judgement: 20,
|
|
judgment: 20,
|
|
universe: 21,
|
|
world: 21,
|
|
"the world": 21
|
|
};
|
|
|
|
const HEBREW_LETTER_ALIASES = {
|
|
aleph: "alef",
|
|
alef: "alef",
|
|
heh: "he",
|
|
he: "he",
|
|
beth: "bet",
|
|
bet: "bet",
|
|
cheth: "het",
|
|
chet: "het",
|
|
kaph: "kaf",
|
|
kaf: "kaf",
|
|
peh: "pe",
|
|
tzaddi: "tsadi",
|
|
tzadi: "tsadi",
|
|
tsadi: "tsadi",
|
|
qoph: "qof",
|
|
qof: "qof",
|
|
taw: "tav",
|
|
tau: "tav"
|
|
};
|
|
|
|
const CUBE_MOTHER_CONNECTOR_BY_LETTER = {
|
|
alef: { connectorId: "above-below", connectorName: "Above ↔ Below" },
|
|
mem: { connectorId: "east-west", connectorName: "East ↔ West" },
|
|
shin: { connectorId: "south-north", connectorName: "South ↔ North" }
|
|
};
|
|
|
|
const MINOR_RANK_NUMBER_BY_NAME = {
|
|
ace: 1,
|
|
two: 2,
|
|
three: 3,
|
|
four: 4,
|
|
five: 5,
|
|
six: 6,
|
|
seven: 7,
|
|
eight: 8,
|
|
nine: 9,
|
|
ten: 10
|
|
};
|
|
|
|
function normalizeRelationId(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/(^-|-$)/g, "");
|
|
}
|
|
|
|
function normalizeTarotName(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^key\s+\d+\s*:\s*/g, "")
|
|
.replace(/\b(pentacles?|coins?)\b/g, "disks")
|
|
.replace(/\s+/g, " ");
|
|
}
|
|
|
|
function normalizeHebrewLetterId(value) {
|
|
const key = String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z]/g, "");
|
|
return HEBREW_LETTER_ALIASES[key] || key;
|
|
}
|
|
|
|
function resolveTarotTrumpNumber(cardName) {
|
|
const rawKey = String(cardName || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
const keyMatch = rawKey.match(/\bkey\s*(\d{1,2})\b/);
|
|
if (keyMatch) {
|
|
return Number(keyMatch[1]);
|
|
}
|
|
|
|
const key = normalizeTarotName(cardName);
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) {
|
|
return TAROT_TRUMP_NUMBER_BY_NAME[key];
|
|
}
|
|
const withoutLeadingThe = key.replace(/^the\s+/, "");
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) {
|
|
return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function cardMatchesTarotAssociation(card, tarotCardName) {
|
|
const associationName = normalizeTarotName(tarotCardName);
|
|
if (!associationName || !card) {
|
|
return false;
|
|
}
|
|
|
|
const cardName = normalizeTarotName(card.name);
|
|
const cardBare = cardName.replace(/^the\s+/, "");
|
|
const assocBare = associationName.replace(/^the\s+/, "");
|
|
|
|
if (
|
|
associationName === cardName ||
|
|
associationName === cardBare ||
|
|
assocBare === cardName ||
|
|
assocBare === cardBare
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (card.arcana === "Major" && Number.isFinite(Number(card.number))) {
|
|
const trumpNumber = resolveTarotTrumpNumber(associationName);
|
|
if (trumpNumber != null) {
|
|
return trumpNumber === Number(card.number);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function buildCourtCardByDecanId(cards) {
|
|
const map = new Map();
|
|
|
|
(cards || []).forEach((card) => {
|
|
if (!card || card.arcana !== "Minor") {
|
|
return;
|
|
}
|
|
|
|
const rankKey = String(card.rank || "").trim().toLowerCase();
|
|
if (rankKey !== "knight" && rankKey !== "queen" && rankKey !== "prince") {
|
|
return;
|
|
}
|
|
|
|
const windowRelation = (Array.isArray(card.relations) ? card.relations : [])
|
|
.find((relation) => relation && typeof relation === "object" && relation.type === "courtDateWindow");
|
|
|
|
const decanIds = Array.isArray(windowRelation?.data?.decanIds)
|
|
? windowRelation.data.decanIds
|
|
: [];
|
|
|
|
decanIds.forEach((decanId) => {
|
|
const decanKey = normalizeRelationId(decanId);
|
|
if (!decanKey || map.has(decanKey)) {
|
|
return;
|
|
}
|
|
|
|
map.set(decanKey, {
|
|
cardName: card.name,
|
|
rank: card.rank,
|
|
suit: card.suit,
|
|
dateRange: String(windowRelation?.data?.dateRange || "").trim()
|
|
});
|
|
});
|
|
});
|
|
|
|
return map;
|
|
}
|
|
|
|
function buildSmallCardCourtLinkRelations(card, relations, courtCardByDecanId) {
|
|
if (!card || card.arcana !== "Minor") {
|
|
return [];
|
|
}
|
|
|
|
const rankKey = String(card.rank || "").trim().toLowerCase();
|
|
const rankNumber = MINOR_RANK_NUMBER_BY_NAME[rankKey];
|
|
if (!Number.isFinite(rankNumber) || rankNumber < 2 || rankNumber > 10) {
|
|
return [];
|
|
}
|
|
|
|
const decans = (relations || []).filter((relation) => relation?.type === "decan");
|
|
if (!decans.length) {
|
|
return [];
|
|
}
|
|
|
|
const results = [];
|
|
const seenCourtCardNames = new Set();
|
|
|
|
decans.forEach((decan) => {
|
|
const signId = String(decan?.data?.signId || "").trim().toLowerCase();
|
|
const decanIndex = Number(decan?.data?.index);
|
|
if (!signId || !Number.isFinite(decanIndex)) {
|
|
return;
|
|
}
|
|
|
|
const decanId = normalizeRelationId(`${signId}-${decanIndex}`);
|
|
const linkedCourt = courtCardByDecanId?.get(decanId);
|
|
if (!linkedCourt?.cardName || seenCourtCardNames.has(linkedCourt.cardName)) {
|
|
return;
|
|
}
|
|
|
|
seenCourtCardNames.add(linkedCourt.cardName);
|
|
|
|
results.push({
|
|
type: "tarotCard",
|
|
id: `${decanId}-${normalizeRelationId(linkedCourt.cardName)}`,
|
|
label: `Shared court date window: ${linkedCourt.cardName}${linkedCourt.dateRange ? ` · ${linkedCourt.dateRange}` : ""}`,
|
|
data: {
|
|
cardName: linkedCourt.cardName,
|
|
dateRange: linkedCourt.dateRange || "",
|
|
decanId
|
|
},
|
|
__key: `tarotCard|${decanId}|${normalizeRelationId(linkedCourt.cardName)}`
|
|
});
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
function parseMonthDayToken(value) {
|
|
const text = String(value || "").trim();
|
|
const match = text.match(/^(\d{1,2})-(\d{1,2})$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const month = Number(match[1]);
|
|
const day = Number(match[2]);
|
|
if (!Number.isInteger(month) || !Number.isInteger(day) || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
return null;
|
|
}
|
|
|
|
return { month, day };
|
|
}
|
|
|
|
function toReferenceDate(token, year) {
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
return new Date(year, token.month - 1, token.day, 12, 0, 0, 0);
|
|
}
|
|
|
|
function splitMonthDayRangeByMonth(startToken, endToken) {
|
|
const startDate = toReferenceDate(startToken, 2025);
|
|
const endBase = toReferenceDate(endToken, 2025);
|
|
if (!startDate || !endBase) {
|
|
return [];
|
|
}
|
|
|
|
const wrapsYear = endBase.getTime() < startDate.getTime();
|
|
const endDate = wrapsYear ? toReferenceDate(endToken, 2026) : endBase;
|
|
if (!endDate) {
|
|
return [];
|
|
}
|
|
|
|
const segments = [];
|
|
let cursor = new Date(startDate);
|
|
|
|
while (cursor.getTime() <= endDate.getTime()) {
|
|
const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0);
|
|
const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate;
|
|
|
|
segments.push({
|
|
monthNo: cursor.getMonth() + 1,
|
|
startDay: cursor.getDate(),
|
|
endDay: segmentEnd.getDate()
|
|
});
|
|
|
|
cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0);
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
function formatMonthDayRangeLabel(monthName, startDay, endDay) {
|
|
const start = Number(startDay);
|
|
const end = Number(endDay);
|
|
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
return monthName;
|
|
}
|
|
if (start === end) {
|
|
return `${monthName} ${start}`;
|
|
}
|
|
return `${monthName} ${start}-${end}`;
|
|
}
|
|
|
|
function buildMonthReferencesByCard(referenceData, cards) {
|
|
const map = new Map();
|
|
const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [];
|
|
const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
|
const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : [];
|
|
const monthById = new Map(months.map((month) => [month.id, month]));
|
|
|
|
function parseMonthFromDateToken(value) {
|
|
const token = parseMonthDayToken(value);
|
|
return token ? token.month : null;
|
|
}
|
|
|
|
function findMonthByNumber(monthNo) {
|
|
if (!Number.isInteger(monthNo) || monthNo < 1 || monthNo > 12) {
|
|
return null;
|
|
}
|
|
|
|
const byOrder = months.find((month) => Number(month?.order) === monthNo);
|
|
if (byOrder) {
|
|
return byOrder;
|
|
}
|
|
|
|
return months.find((month) => parseMonthFromDateToken(month?.start) === monthNo) || null;
|
|
}
|
|
|
|
function pushRef(card, month, options = {}) {
|
|
if (!card?.id || !month?.id) {
|
|
return;
|
|
}
|
|
|
|
if (!map.has(card.id)) {
|
|
map.set(card.id, []);
|
|
}
|
|
|
|
const rows = map.get(card.id);
|
|
const monthOrder = Number.isFinite(Number(month.order)) ? Number(month.order) : 999;
|
|
const startToken = parseMonthDayToken(options.startToken || month.start);
|
|
const endToken = parseMonthDayToken(options.endToken || month.end);
|
|
const dateRange = String(options.dateRange || "").trim() || (
|
|
startToken && endToken
|
|
? formatMonthDayRangeLabel(month.name || month.id, startToken.day, endToken.day)
|
|
: ""
|
|
);
|
|
|
|
const uniqueKey = [
|
|
month.id,
|
|
dateRange.toLowerCase(),
|
|
String(options.context || "").trim().toLowerCase(),
|
|
String(options.source || "").trim().toLowerCase()
|
|
].join("|");
|
|
|
|
if (rows.some((entry) => entry.uniqueKey === uniqueKey)) {
|
|
return;
|
|
}
|
|
|
|
rows.push({
|
|
id: month.id,
|
|
name: month.name || month.id,
|
|
order: monthOrder,
|
|
startToken: startToken ? `${String(startToken.month).padStart(2, "0")}-${String(startToken.day).padStart(2, "0")}` : null,
|
|
endToken: endToken ? `${String(endToken.month).padStart(2, "0")}-${String(endToken.day).padStart(2, "0")}` : null,
|
|
dateRange,
|
|
context: String(options.context || "").trim(),
|
|
source: String(options.source || "").trim(),
|
|
uniqueKey
|
|
});
|
|
}
|
|
|
|
function captureRefs(associations, month) {
|
|
const tarotCardName = associations?.tarotCard;
|
|
if (!tarotCardName) {
|
|
return;
|
|
}
|
|
|
|
cards.forEach((card) => {
|
|
if (cardMatchesTarotAssociation(card, tarotCardName)) {
|
|
pushRef(card, month);
|
|
}
|
|
});
|
|
}
|
|
|
|
months.forEach((month) => {
|
|
captureRefs(month?.associations, month);
|
|
|
|
const events = Array.isArray(month?.events) ? month.events : [];
|
|
events.forEach((event) => {
|
|
const tarotCardName = event?.associations?.tarotCard;
|
|
if (!tarotCardName) {
|
|
return;
|
|
}
|
|
cards.forEach((card) => {
|
|
if (!cardMatchesTarotAssociation(card, tarotCardName)) {
|
|
return;
|
|
}
|
|
pushRef(card, month, {
|
|
source: "month-event",
|
|
context: String(event?.name || "").trim()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
holidays.forEach((holiday) => {
|
|
const month = monthById.get(holiday?.monthId);
|
|
if (!month) {
|
|
return;
|
|
}
|
|
const tarotCardName = holiday?.associations?.tarotCard;
|
|
if (!tarotCardName) {
|
|
return;
|
|
}
|
|
cards.forEach((card) => {
|
|
if (!cardMatchesTarotAssociation(card, tarotCardName)) {
|
|
return;
|
|
}
|
|
pushRef(card, month, {
|
|
source: "holiday",
|
|
context: String(holiday?.name || "").trim()
|
|
});
|
|
});
|
|
});
|
|
|
|
signs.forEach((sign) => {
|
|
const signTrumpNumber = Number(sign?.tarot?.number);
|
|
const signTarotName = sign?.tarot?.majorArcana || sign?.tarot?.card || sign?.tarotCard;
|
|
if (!Number.isFinite(signTrumpNumber) && !signTarotName) {
|
|
return;
|
|
}
|
|
|
|
const signName = String(sign?.name || sign?.id || "").trim();
|
|
const startToken = parseMonthDayToken(sign?.start);
|
|
const endToken = parseMonthDayToken(sign?.end);
|
|
const monthSegments = splitMonthDayRangeByMonth(startToken, endToken);
|
|
const fallbackStartMonthNo = parseMonthFromDateToken(sign?.start);
|
|
const fallbackEndMonthNo = parseMonthFromDateToken(sign?.end);
|
|
const fallbackStartMonth = findMonthByNumber(fallbackStartMonthNo);
|
|
const fallbackEndMonth = findMonthByNumber(fallbackEndMonthNo);
|
|
|
|
cards.forEach((card) => {
|
|
const cardTrumpNumber = Number(card?.number);
|
|
const matchesByTrump = card?.arcana === "Major"
|
|
&& Number.isFinite(cardTrumpNumber)
|
|
&& Number.isFinite(signTrumpNumber)
|
|
&& cardTrumpNumber === signTrumpNumber;
|
|
const matchesByName = signTarotName ? cardMatchesTarotAssociation(card, signTarotName) : false;
|
|
|
|
if (!matchesByTrump && !matchesByName) {
|
|
return;
|
|
}
|
|
|
|
if (monthSegments.length) {
|
|
monthSegments.forEach((segment) => {
|
|
const month = findMonthByNumber(segment.monthNo);
|
|
if (!month) {
|
|
return;
|
|
}
|
|
|
|
pushRef(card, month, {
|
|
source: "zodiac-window",
|
|
context: signName ? `${signName} window` : "",
|
|
startToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.startDay).padStart(2, "0")}`,
|
|
endToken: `${String(segment.monthNo).padStart(2, "0")}-${String(segment.endDay).padStart(2, "0")}`,
|
|
dateRange: formatMonthDayRangeLabel(month.name || month.id, segment.startDay, segment.endDay)
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (fallbackStartMonth) {
|
|
pushRef(card, fallbackStartMonth, {
|
|
source: "zodiac-window",
|
|
context: signName ? `${signName} window` : ""
|
|
});
|
|
}
|
|
if (fallbackEndMonth && (!fallbackStartMonth || fallbackEndMonth.id !== fallbackStartMonth.id)) {
|
|
pushRef(card, fallbackEndMonth, {
|
|
source: "zodiac-window",
|
|
context: signName ? `${signName} window` : ""
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
map.forEach((rows, key) => {
|
|
const monthIdsWithZodiacWindows = new Set(
|
|
rows
|
|
.filter((entry) => entry?.source === "zodiac-window" && entry?.id)
|
|
.map((entry) => entry.id)
|
|
);
|
|
|
|
const filteredRows = rows.filter((entry) => {
|
|
if (!entry?.id || entry?.source === "zodiac-window") {
|
|
return true;
|
|
}
|
|
|
|
if (!monthIdsWithZodiacWindows.has(entry.id)) {
|
|
return true;
|
|
}
|
|
|
|
const month = monthById.get(entry.id);
|
|
if (!month) {
|
|
return true;
|
|
}
|
|
|
|
const isFullMonth = String(entry.startToken || "") === String(month.start || "")
|
|
&& String(entry.endToken || "") === String(month.end || "");
|
|
|
|
return !isFullMonth;
|
|
});
|
|
|
|
filteredRows.sort((left, right) => {
|
|
if (left.order !== right.order) {
|
|
return left.order - right.order;
|
|
}
|
|
|
|
const startLeft = parseMonthDayToken(left.startToken);
|
|
const startRight = parseMonthDayToken(right.startToken);
|
|
const dayLeft = startLeft ? startLeft.day : 999;
|
|
const dayRight = startRight ? startRight.day : 999;
|
|
if (dayLeft !== dayRight) {
|
|
return dayLeft - dayRight;
|
|
}
|
|
|
|
return String(left.dateRange || left.name || "").localeCompare(String(right.dateRange || right.name || ""));
|
|
});
|
|
map.set(key, filteredRows);
|
|
});
|
|
|
|
return map;
|
|
}
|
|
|
|
function buildCubeFaceRelationsForCard(card, magickDataset) {
|
|
const cube = magickDataset?.grouped?.kabbalah?.cube;
|
|
const walls = Array.isArray(cube?.walls) ? cube.walls : [];
|
|
if (!card || !walls.length) {
|
|
return [];
|
|
}
|
|
|
|
return walls
|
|
.map((wall, index) => {
|
|
const wallTarot = wall?.associations?.tarotCard || wall?.tarotCard;
|
|
if (!wallTarot || !cardMatchesTarotAssociation(card, wallTarot)) {
|
|
return null;
|
|
}
|
|
|
|
const wallId = String(wall?.id || "").trim();
|
|
const wallName = String(wall?.name || wallId || "").trim();
|
|
if (!wallId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type: "cubeFace",
|
|
id: wallId,
|
|
label: `Cube: ${wallName} Wall - Face`,
|
|
data: {
|
|
wallId,
|
|
wallName,
|
|
edgeId: ""
|
|
},
|
|
__key: `cubeFace|${wallId}|${index}`
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function cardMatchesPathTarot(card, path) {
|
|
if (!card || !path) {
|
|
return false;
|
|
}
|
|
|
|
const trumpNumber = Number(path?.tarot?.trumpNumber);
|
|
if (card?.arcana === "Major" && Number.isFinite(Number(card?.number)) && Number.isFinite(trumpNumber)) {
|
|
if (Number(card.number) === trumpNumber) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return cardMatchesTarotAssociation(card, path?.tarot?.card);
|
|
}
|
|
|
|
function buildCubeEdgeRelationsForCard(card, magickDataset) {
|
|
const cube = magickDataset?.grouped?.kabbalah?.cube;
|
|
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
|
|
const edges = Array.isArray(cube?.edges) ? cube.edges : [];
|
|
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
|
|
|
|
if (!card || !edges.length || !paths.length) {
|
|
return [];
|
|
}
|
|
|
|
const pathByLetterId = new Map(
|
|
paths
|
|
.map((path) => [normalizeHebrewLetterId(path?.hebrewLetter?.transliteration), path])
|
|
.filter(([letterId]) => Boolean(letterId))
|
|
);
|
|
|
|
return edges
|
|
.map((edge, index) => {
|
|
const edgeLetterId = normalizeHebrewLetterId(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId);
|
|
if (!edgeLetterId) {
|
|
return null;
|
|
}
|
|
|
|
const pathMatch = pathByLetterId.get(edgeLetterId);
|
|
if (!pathMatch || !cardMatchesPathTarot(card, pathMatch)) {
|
|
return null;
|
|
}
|
|
|
|
const edgeId = String(edge?.id || "").trim();
|
|
if (!edgeId) {
|
|
return null;
|
|
}
|
|
|
|
const edgeName = String(edge?.name || edgeId).trim();
|
|
const wallId = String(Array.isArray(edge?.walls) ? (edge.walls[0] || "") : "").trim();
|
|
|
|
return {
|
|
type: "cubeEdge",
|
|
id: edgeId,
|
|
label: `Cube: ${edgeName} Edge`,
|
|
data: {
|
|
edgeId,
|
|
edgeName,
|
|
wallId: wallId || undefined,
|
|
hebrewLetterId: edgeLetterId
|
|
},
|
|
__key: `cubeEdge|${edgeId}|${index}`
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function buildCubeMotherConnectorRelationsForCard(card, magickDataset) {
|
|
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
|
|
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
|
|
const relations = Array.isArray(card?.relations) ? card.relations : [];
|
|
|
|
return Object.entries(CUBE_MOTHER_CONNECTOR_BY_LETTER)
|
|
.map(([letterId, connector]) => {
|
|
const pathMatch = paths.find((path) => normalizeHebrewLetterId(path?.hebrewLetter?.transliteration) === letterId) || null;
|
|
|
|
const matchesByPath = cardMatchesPathTarot(card, pathMatch);
|
|
|
|
const matchesByHebrewRelation = relations.some((relation) => {
|
|
if (relation?.type !== "hebrewLetter") {
|
|
return false;
|
|
}
|
|
const relationLetterId = normalizeHebrewLetterId(
|
|
relation?.data?.id || relation?.id || relation?.data?.latin || relation?.data?.name
|
|
);
|
|
return relationLetterId === letterId;
|
|
});
|
|
|
|
if (!matchesByPath && !matchesByHebrewRelation) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type: "cubeConnector",
|
|
id: connector.connectorId,
|
|
label: `Cube: ${connector.connectorName}`,
|
|
data: {
|
|
connectorId: connector.connectorId,
|
|
connectorName: connector.connectorName,
|
|
hebrewLetterId: letterId
|
|
},
|
|
__key: `cubeConnector|${connector.connectorId}|${letterId}`
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function buildCubePrimalPointRelationsForCard(card, magickDataset) {
|
|
const center = magickDataset?.grouped?.kabbalah?.cube?.center;
|
|
if (!center || !card) {
|
|
return [];
|
|
}
|
|
|
|
const centerTarot = center?.associations?.tarotCard || center?.tarotCard;
|
|
const centerTrump = Number(center?.associations?.tarotTrumpNumber);
|
|
const matchesByName = cardMatchesTarotAssociation(card, centerTarot);
|
|
const matchesByTrump = card?.arcana === "Major"
|
|
&& Number.isFinite(Number(card?.number))
|
|
&& Number.isFinite(centerTrump)
|
|
&& Number(card.number) === centerTrump;
|
|
|
|
if (!matchesByName && !matchesByTrump) {
|
|
return [];
|
|
}
|
|
|
|
return [{
|
|
type: "cubeCenter",
|
|
id: "primal-point",
|
|
label: "Cube: Primal Point",
|
|
data: {
|
|
nodeType: "center",
|
|
primalPoint: true
|
|
},
|
|
__key: "cubeCenter|primal-point"
|
|
}];
|
|
}
|
|
|
|
function buildCubeRelationsForCard(card, magickDataset) {
|
|
return [
|
|
...buildCubeFaceRelationsForCard(card, magickDataset),
|
|
...buildCubeEdgeRelationsForCard(card, magickDataset),
|
|
...buildCubePrimalPointRelationsForCard(card, magickDataset),
|
|
...buildCubeMotherConnectorRelationsForCard(card, magickDataset)
|
|
];
|
|
}
|
|
|
|
function buildIChingRelationsForCard(card, referenceData) {
|
|
const iChing = referenceData?.iChing;
|
|
const hexagrams = Array.isArray(iChing?.hexagrams) ? iChing.hexagrams : [];
|
|
const trigrams = Array.isArray(iChing?.trigrams) ? iChing.trigrams : [];
|
|
const correspondences = Array.isArray(iChing?.correspondences?.tarotToTrigram)
|
|
? iChing.correspondences.tarotToTrigram
|
|
: [];
|
|
|
|
if (!card || !hexagrams.length || !correspondences.length) {
|
|
return [];
|
|
}
|
|
|
|
const trigramByKey = new Map(
|
|
trigrams
|
|
.map((trigram) => [normalizeRelationId(trigram?.name), trigram])
|
|
.filter(([key]) => Boolean(key))
|
|
);
|
|
|
|
const matchedTrigramKeys = [...new Set(
|
|
correspondences
|
|
.filter((row) => cardMatchesTarotAssociation(card, row?.tarot))
|
|
.map((row) => normalizeRelationId(row?.trigram))
|
|
.filter(Boolean)
|
|
)];
|
|
|
|
if (!matchedTrigramKeys.length) {
|
|
return [];
|
|
}
|
|
|
|
const relations = [];
|
|
|
|
hexagrams.forEach((hexagram) => {
|
|
const positionsByTrigram = new Map();
|
|
const upperKey = normalizeRelationId(hexagram?.upperTrigram);
|
|
const lowerKey = normalizeRelationId(hexagram?.lowerTrigram);
|
|
|
|
if (matchedTrigramKeys.includes(upperKey)) {
|
|
positionsByTrigram.set(upperKey, ["upper"]);
|
|
}
|
|
if (matchedTrigramKeys.includes(lowerKey)) {
|
|
const existing = positionsByTrigram.get(lowerKey) || [];
|
|
positionsByTrigram.set(lowerKey, [...existing, "lower"]);
|
|
}
|
|
|
|
positionsByTrigram.forEach((positions, trigramKey) => {
|
|
const trigram = trigramByKey.get(trigramKey) || null;
|
|
const sideLabel = positions.length === 2
|
|
? "Upper + Lower"
|
|
: positions[0] === "upper"
|
|
? "Upper"
|
|
: "Lower";
|
|
const trigramLabel = trigram?.element || trigram?.name || hexagram?.upperTrigram || hexagram?.lowerTrigram || "Trigram";
|
|
|
|
relations.push({
|
|
type: "ichingHexagram",
|
|
id: String(hexagram?.number || ""),
|
|
label: `Hexagram ${hexagram.number}: ${hexagram.name || "--"} · ${sideLabel} ${trigramLabel}`,
|
|
data: {
|
|
hexagramNumber: Number(hexagram?.number),
|
|
name: hexagram?.name || "",
|
|
upperTrigram: hexagram?.upperTrigram || "",
|
|
lowerTrigram: hexagram?.lowerTrigram || "",
|
|
trigramName: trigram?.name || "",
|
|
trigramElement: trigram?.element || "",
|
|
positions: positions.join(",")
|
|
},
|
|
__key: `ichingHexagram|${hexagram?.number}|${trigramKey}|${positions.join(",")}`
|
|
});
|
|
});
|
|
});
|
|
|
|
return relations.sort((left, right) => Number(left?.data?.hexagramNumber || 999) - Number(right?.data?.hexagramNumber || 999));
|
|
}
|
|
|
|
window.TarotRelationsUi = {
|
|
buildCourtCardByDecanId,
|
|
buildSmallCardCourtLinkRelations,
|
|
buildMonthReferencesByCard,
|
|
buildCubeRelationsForCard,
|
|
buildIChingRelationsForCard,
|
|
parseMonthDayToken
|
|
};
|
|
})(); |