867 lines
25 KiB
JavaScript
867 lines
25 KiB
JavaScript
(function () {
|
|
const DEFAULT_DECK_ID = "ceremonial-magick";
|
|
|
|
const trumpNumberByCanonicalName = {
|
|
fool: 0,
|
|
magus: 1,
|
|
magician: 1,
|
|
"high priestess": 2,
|
|
empress: 3,
|
|
emperor: 4,
|
|
hierophant: 5,
|
|
lovers: 6,
|
|
chariot: 7,
|
|
lust: 8,
|
|
strength: 8,
|
|
hermit: 9,
|
|
fortune: 10,
|
|
"wheel of fortune": 10,
|
|
justice: 11,
|
|
"hanged man": 12,
|
|
death: 13,
|
|
art: 14,
|
|
temperance: 14,
|
|
devil: 15,
|
|
tower: 16,
|
|
star: 17,
|
|
moon: 18,
|
|
sun: 19,
|
|
aeon: 20,
|
|
judgement: 20,
|
|
judgment: 20,
|
|
universe: 21,
|
|
world: 21
|
|
};
|
|
|
|
const pipValueByToken = {
|
|
ace: 1,
|
|
two: 2,
|
|
three: 3,
|
|
four: 4,
|
|
five: 5,
|
|
six: 6,
|
|
seven: 7,
|
|
eight: 8,
|
|
nine: 9,
|
|
ten: 10,
|
|
"2": 2,
|
|
"3": 3,
|
|
"4": 4,
|
|
"5": 5,
|
|
"6": 6,
|
|
"7": 7,
|
|
"8": 8,
|
|
"9": 9,
|
|
"10": 10
|
|
};
|
|
|
|
const rankWordByPipValue = {
|
|
1: "Ace",
|
|
2: "Two",
|
|
3: "Three",
|
|
4: "Four",
|
|
5: "Five",
|
|
6: "Six",
|
|
7: "Seven",
|
|
8: "Eight",
|
|
9: "Nine",
|
|
10: "Ten"
|
|
};
|
|
|
|
const trumpRomanToNumber = {
|
|
I: 1,
|
|
II: 2,
|
|
III: 3,
|
|
IV: 4,
|
|
V: 5,
|
|
VI: 6,
|
|
VII: 7,
|
|
VIII: 8,
|
|
IX: 9,
|
|
X: 10,
|
|
XI: 11,
|
|
XII: 12,
|
|
XIII: 13,
|
|
XIV: 14,
|
|
XV: 15,
|
|
XVI: 16,
|
|
XVII: 17,
|
|
XVIII: 18,
|
|
XIX: 19,
|
|
XX: 20,
|
|
XXI: 21
|
|
};
|
|
|
|
const suitSearchAliasesById = {
|
|
wands: ["wands"],
|
|
cups: ["cups"],
|
|
swords: ["swords"],
|
|
disks: ["disks", "pentacles", "coins"]
|
|
};
|
|
const defaultPipRankOrder = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"];
|
|
const defaultThumbnailConfig = {
|
|
root: "thumbs",
|
|
width: 240,
|
|
height: 360,
|
|
fit: "inside",
|
|
quality: 82
|
|
};
|
|
|
|
const DECK_REGISTRY_PATH = "asset/tarot deck/decks.json";
|
|
|
|
let deckManifestSources = buildDeckManifestSources();
|
|
|
|
const manifestCache = new Map();
|
|
const cardBackCache = new Map();
|
|
const cardBackThumbnailCache = new Map();
|
|
let activeDeckId = DEFAULT_DECK_ID;
|
|
|
|
function canonicalMajorName(cardName) {
|
|
return String(cardName || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^the\s+/, "")
|
|
.replace(/\s+/g, " ");
|
|
}
|
|
|
|
function canonicalMinorName(cardName) {
|
|
const parsedMinor = parseMinorCard(cardName);
|
|
if (!parsedMinor) {
|
|
return "";
|
|
}
|
|
|
|
return `${String(parsedMinor.rankKey || "").trim().toLowerCase()} of ${parsedMinor.suitId}`;
|
|
}
|
|
|
|
function toTitleCase(value) {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
}
|
|
|
|
function normalizeDeckId(deckId) {
|
|
const sources = getDeckManifestSources();
|
|
const normalized = String(deckId || "").trim().toLowerCase();
|
|
if (sources[normalized]) {
|
|
return normalized;
|
|
}
|
|
|
|
if (sources[DEFAULT_DECK_ID]) {
|
|
return DEFAULT_DECK_ID;
|
|
}
|
|
|
|
const fallbackId = Object.keys(sources)[0];
|
|
return fallbackId || DEFAULT_DECK_ID;
|
|
}
|
|
|
|
function normalizeTrumpNumber(value) {
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 21) {
|
|
return null;
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function parseTrumpNumberKey(value) {
|
|
const normalized = String(value || "").trim().toUpperCase();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
if (/^\d+$/.test(normalized)) {
|
|
return normalizeTrumpNumber(Number(normalized));
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(trumpRomanToNumber, normalized)) {
|
|
return normalizeTrumpNumber(trumpRomanToNumber[normalized]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function normalizeSuitId(suitInput) {
|
|
const suit = String(suitInput || "").trim().toLowerCase();
|
|
if (suit === "pentacles") {
|
|
return "disks";
|
|
}
|
|
return suit;
|
|
}
|
|
|
|
function resolveDeckOptions(optionsOrDeckId) {
|
|
let resolvedDeckId = activeDeckId;
|
|
let trumpNumber = null;
|
|
|
|
if (typeof optionsOrDeckId === "string") {
|
|
resolvedDeckId = normalizeDeckId(optionsOrDeckId);
|
|
} else if (optionsOrDeckId && typeof optionsOrDeckId === "object") {
|
|
if (optionsOrDeckId.deckId) {
|
|
resolvedDeckId = normalizeDeckId(optionsOrDeckId.deckId);
|
|
}
|
|
trumpNumber = normalizeTrumpNumber(optionsOrDeckId.trumpNumber);
|
|
}
|
|
|
|
return { resolvedDeckId, trumpNumber };
|
|
}
|
|
|
|
function parseMinorCard(cardName) {
|
|
const match = String(cardName || "")
|
|
.trim()
|
|
.match(/^(ace|two|three|four|five|six|seven|eight|nine|ten|knight|queen|prince|princess|king|page|[2-9]|10)\s+of\s+(cups|wands|swords|pentacles|disks)$/i);
|
|
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const rankToken = String(match[1] || "").toLowerCase();
|
|
const suitId = normalizeSuitId(match[2]);
|
|
const pipValue = pipValueByToken[rankToken] ?? null;
|
|
|
|
if (Number.isFinite(pipValue)) {
|
|
const rankWord = rankWordByPipValue[pipValue] || "";
|
|
return {
|
|
suitId,
|
|
pipValue,
|
|
court: "",
|
|
rankWord,
|
|
rankKey: rankWord.toLowerCase()
|
|
};
|
|
}
|
|
|
|
const courtWord = toTitleCase(rankToken);
|
|
if (!courtWord) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
suitId,
|
|
pipValue: null,
|
|
court: rankToken,
|
|
rankWord: courtWord,
|
|
rankKey: rankToken
|
|
};
|
|
}
|
|
|
|
function applyTemplate(template, variables) {
|
|
return String(template || "")
|
|
.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, token) => {
|
|
const value = variables[token];
|
|
return value == null ? "" : String(value);
|
|
});
|
|
}
|
|
|
|
function isRemoteAssetPath(pathValue) {
|
|
return /^(https?:)?\/\//i.test(String(pathValue || ""));
|
|
}
|
|
|
|
function toDeckAssetPath(manifest, relativeOrAbsolutePath) {
|
|
const normalizedPath = String(relativeOrAbsolutePath || "").trim();
|
|
if (!normalizedPath) {
|
|
return "";
|
|
}
|
|
|
|
if (isRemoteAssetPath(normalizedPath) || normalizedPath.startsWith("/")) {
|
|
return normalizedPath;
|
|
}
|
|
|
|
return `${manifest.basePath}/${normalizedPath.replace(/^\.\//, "")}`;
|
|
}
|
|
|
|
function resolveDeckCardBackPath(manifest) {
|
|
if (!manifest) {
|
|
return null;
|
|
}
|
|
|
|
const explicitCardBack = String(manifest.cardBack || "").trim();
|
|
if (explicitCardBack) {
|
|
return toDeckAssetPath(manifest, explicitCardBack) || null;
|
|
}
|
|
|
|
const detectedCardBack = String(manifest.cardBackPath || "").trim();
|
|
if (detectedCardBack) {
|
|
return toDeckAssetPath(manifest, detectedCardBack) || null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function normalizeThumbnailConfig(rawConfig, fallbackRoot = "") {
|
|
if (rawConfig === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!rawConfig || typeof rawConfig !== "object") {
|
|
const root = String(fallbackRoot || "").trim();
|
|
if (!root) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...defaultThumbnailConfig,
|
|
root
|
|
};
|
|
}
|
|
|
|
const root = String(rawConfig.root || fallbackRoot || defaultThumbnailConfig.root).trim();
|
|
if (!root) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
root,
|
|
width: Number.isInteger(Number(rawConfig.width)) && Number(rawConfig.width) > 0
|
|
? Number(rawConfig.width)
|
|
: defaultThumbnailConfig.width,
|
|
height: Number.isInteger(Number(rawConfig.height)) && Number(rawConfig.height) > 0
|
|
? Number(rawConfig.height)
|
|
: defaultThumbnailConfig.height,
|
|
fit: String(rawConfig.fit || defaultThumbnailConfig.fit).trim() || defaultThumbnailConfig.fit,
|
|
quality: Number.isInteger(Number(rawConfig.quality)) && Number(rawConfig.quality) >= 1 && Number(rawConfig.quality) <= 100
|
|
? Number(rawConfig.quality)
|
|
: defaultThumbnailConfig.quality
|
|
};
|
|
}
|
|
|
|
function resolveDeckThumbnailPath(manifest, relativePath) {
|
|
if (!manifest || !manifest.thumbnails || manifest.thumbnails === false) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedPath = String(relativePath || "").trim().replace(/^\.\//, "");
|
|
if (!normalizedPath || isRemoteAssetPath(normalizedPath) || normalizedPath.startsWith("/")) {
|
|
return null;
|
|
}
|
|
|
|
return toDeckAssetPath(manifest, `${manifest.thumbnails.root}/${normalizedPath}`) || null;
|
|
}
|
|
|
|
function readManifestJsonSync(path) {
|
|
try {
|
|
const request = new XMLHttpRequest();
|
|
request.open("GET", encodeURI(path), false);
|
|
request.send(null);
|
|
|
|
const okStatus = (request.status >= 200 && request.status < 300) || request.status === 0;
|
|
if (!okStatus || !request.responseText) {
|
|
return null;
|
|
}
|
|
|
|
return JSON.parse(request.responseText);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toDeckSourceMap(sourceList) {
|
|
const sourceMap = {};
|
|
if (!Array.isArray(sourceList)) {
|
|
return sourceMap;
|
|
}
|
|
|
|
sourceList.forEach((entry) => {
|
|
const id = String(entry?.id || "").trim().toLowerCase();
|
|
const basePath = String(entry?.basePath || "").trim().replace(/\/$/, "");
|
|
const manifestPath = String(entry?.manifestPath || "").trim();
|
|
if (!id || !basePath || !manifestPath) {
|
|
return;
|
|
}
|
|
|
|
sourceMap[id] = {
|
|
id,
|
|
label: String(entry?.label || id),
|
|
basePath,
|
|
manifestPath,
|
|
cardBackPath: String(entry?.cardBackPath || "").trim(),
|
|
thumbnailRoot: String(entry?.thumbnailRoot || "").trim()
|
|
};
|
|
});
|
|
|
|
return sourceMap;
|
|
}
|
|
|
|
function buildDeckManifestSources() {
|
|
const registry = readManifestJsonSync(DECK_REGISTRY_PATH);
|
|
const registryDecks = Array.isArray(registry)
|
|
? registry
|
|
: (Array.isArray(registry?.decks) ? registry.decks : null);
|
|
|
|
return toDeckSourceMap(registryDecks);
|
|
}
|
|
|
|
function getDeckManifestSources(forceRefresh = false) {
|
|
if (forceRefresh || !deckManifestSources || Object.keys(deckManifestSources).length === 0) {
|
|
deckManifestSources = buildDeckManifestSources();
|
|
}
|
|
|
|
return deckManifestSources || {};
|
|
}
|
|
|
|
function normalizeDeckManifest(source, rawManifest) {
|
|
if (!rawManifest || typeof rawManifest !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const rawNameOverrides = rawManifest.nameOverrides;
|
|
const nameOverrides = {};
|
|
if (rawNameOverrides && typeof rawNameOverrides === "object") {
|
|
Object.entries(rawNameOverrides).forEach(([rawKey, rawValue]) => {
|
|
const key = canonicalMajorName(rawKey);
|
|
const value = String(rawValue || "").trim();
|
|
if (key && value) {
|
|
nameOverrides[key] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
const rawMajorNameOverridesByTrump = rawManifest.majorNameOverridesByTrump;
|
|
const majorNameOverridesByTrump = {};
|
|
if (rawMajorNameOverridesByTrump && typeof rawMajorNameOverridesByTrump === "object") {
|
|
Object.entries(rawMajorNameOverridesByTrump).forEach(([rawKey, rawValue]) => {
|
|
const trumpNumber = parseTrumpNumberKey(rawKey);
|
|
const value = String(rawValue || "").trim();
|
|
if (Number.isInteger(trumpNumber) && value) {
|
|
majorNameOverridesByTrump[trumpNumber] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
const rawMinorNameOverrides = rawManifest.minorNameOverrides;
|
|
const minorNameOverrides = {};
|
|
if (rawMinorNameOverrides && typeof rawMinorNameOverrides === "object") {
|
|
Object.entries(rawMinorNameOverrides).forEach(([rawKey, rawValue]) => {
|
|
const key = canonicalMinorName(rawKey);
|
|
const value = String(rawValue || "").trim();
|
|
if (key && value) {
|
|
minorNameOverrides[key] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
id: source.id,
|
|
label: String(rawManifest.label || source.label || source.id),
|
|
basePath: String(source.basePath || "").replace(/\/$/, ""),
|
|
cardBack: String(rawManifest.cardBack || "").trim(),
|
|
cardBackPath: String(source.cardBackPath || "").trim(),
|
|
thumbnails: normalizeThumbnailConfig(rawManifest.thumbnails, source.thumbnailRoot),
|
|
majors: rawManifest.majors || {},
|
|
minors: rawManifest.minors || {},
|
|
nameOverrides,
|
|
minorNameOverrides,
|
|
majorNameOverridesByTrump
|
|
};
|
|
}
|
|
|
|
function getDeckManifest(deckId) {
|
|
const normalizedDeckId = normalizeDeckId(deckId);
|
|
if (manifestCache.has(normalizedDeckId)) {
|
|
return manifestCache.get(normalizedDeckId);
|
|
}
|
|
|
|
let sources = getDeckManifestSources();
|
|
let source = sources[normalizedDeckId];
|
|
if (!source) {
|
|
sources = getDeckManifestSources(true);
|
|
source = sources[normalizedDeckId];
|
|
}
|
|
|
|
if (!source) {
|
|
return null;
|
|
}
|
|
|
|
const rawManifest = readManifestJsonSync(source.manifestPath);
|
|
const normalizedManifest = normalizeDeckManifest(source, rawManifest);
|
|
if (normalizedManifest) {
|
|
manifestCache.set(normalizedDeckId, normalizedManifest);
|
|
}
|
|
return normalizedManifest;
|
|
}
|
|
|
|
function getRankOrder(minorRule, fallbackRankOrder = []) {
|
|
const explicitRankOrder = Array.isArray(minorRule?.rankOrder) ? minorRule.rankOrder : [];
|
|
const rankOrderSource = explicitRankOrder.length ? explicitRankOrder : fallbackRankOrder;
|
|
return rankOrderSource.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
}
|
|
|
|
function getRankIndex(minorRule, parsedMinor, fallbackRankOrder = []) {
|
|
if (!minorRule || !parsedMinor) {
|
|
return null;
|
|
}
|
|
|
|
const lowerRankWord = String(parsedMinor.rankWord || "").toLowerCase();
|
|
const lowerRankKey = String(parsedMinor.rankKey || "").toLowerCase();
|
|
|
|
const indexByKey = minorRule.rankIndexByKey;
|
|
if (indexByKey && typeof indexByKey === "object") {
|
|
const mapped = Number(indexByKey[lowerRankKey]);
|
|
if (Number.isInteger(mapped) && mapped >= 0) {
|
|
return mapped;
|
|
}
|
|
}
|
|
|
|
const rankOrder = getRankOrder(minorRule, fallbackRankOrder);
|
|
for (let i = 0; i < rankOrder.length; i += 1) {
|
|
const candidate = String(rankOrder[i] || "").toLowerCase();
|
|
if (candidate && (candidate === lowerRankWord || candidate === lowerRankKey)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveMinorNumberTemplateGroup(groupRule, parsedMinor, fallbackRankOrder = []) {
|
|
if (!groupRule || typeof groupRule !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const rankIndex = getRankIndex(groupRule, parsedMinor, fallbackRankOrder);
|
|
if (!Number.isInteger(rankIndex) || rankIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
const suitBaseRaw = Number(groupRule?.suitBase?.[parsedMinor.suitId]);
|
|
if (!Number.isFinite(suitBaseRaw)) {
|
|
return null;
|
|
}
|
|
|
|
const numberPad = Number.isInteger(groupRule.numberPad) ? groupRule.numberPad : 2;
|
|
const cardNumber = String(suitBaseRaw + rankIndex).padStart(numberPad, "0");
|
|
const template = String(groupRule.template || "{number}.png");
|
|
|
|
return applyTemplate(template, {
|
|
number: cardNumber,
|
|
suitId: parsedMinor.suitId,
|
|
rank: parsedMinor.rankWord,
|
|
rankKey: parsedMinor.rankKey,
|
|
index: rankIndex
|
|
});
|
|
}
|
|
|
|
function resolveMajorFile(manifest, canonicalName) {
|
|
const majorRule = manifest?.majors;
|
|
if (!majorRule || typeof majorRule !== "object") {
|
|
return null;
|
|
}
|
|
|
|
if (majorRule.mode === "canonical-map") {
|
|
const cards = majorRule.cards || {};
|
|
const fileName = cards[canonicalName];
|
|
return typeof fileName === "string" && fileName ? fileName : null;
|
|
}
|
|
|
|
const trumpNo = trumpNumberByCanonicalName[canonicalName];
|
|
if (!Number.isInteger(trumpNo) || trumpNo < 0 || trumpNo > 21) {
|
|
return null;
|
|
}
|
|
|
|
if (majorRule.mode === "trump-map") {
|
|
const cards = majorRule.cards || {};
|
|
const fileName = cards[String(trumpNo)] ?? cards[trumpNo];
|
|
return typeof fileName === "string" && fileName ? fileName : null;
|
|
}
|
|
|
|
if (majorRule.mode === "trump-template") {
|
|
const numberPad = Number.isInteger(majorRule.numberPad) ? majorRule.numberPad : 2;
|
|
const template = String(majorRule.template || "{number}.png");
|
|
const number = String(trumpNo).padStart(numberPad, "0");
|
|
return applyTemplate(template, {
|
|
trump: trumpNo,
|
|
number
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveMinorFile(manifest, parsedMinor) {
|
|
const minorRule = manifest?.minors;
|
|
if (!minorRule || typeof minorRule !== "object") {
|
|
return null;
|
|
}
|
|
|
|
if (minorRule.mode === "split-number-template") {
|
|
if (Number.isFinite(parsedMinor.pipValue)) {
|
|
return resolveMinorNumberTemplateGroup(minorRule.smalls, parsedMinor, defaultPipRankOrder);
|
|
}
|
|
|
|
return resolveMinorNumberTemplateGroup(minorRule.courts, parsedMinor);
|
|
}
|
|
|
|
const rankIndex = getRankIndex(minorRule, parsedMinor);
|
|
if (!Number.isInteger(rankIndex) || rankIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
if (minorRule.mode === "suit-base-and-rank-order") {
|
|
const suitBaseRaw = Number(minorRule?.suitBase?.[parsedMinor.suitId]);
|
|
if (!Number.isFinite(suitBaseRaw)) {
|
|
return null;
|
|
}
|
|
|
|
const numberPad = Number.isInteger(minorRule.numberPad) ? minorRule.numberPad : 2;
|
|
const cardNumber = String(suitBaseRaw + rankIndex).padStart(numberPad, "0");
|
|
const suitWord = String(minorRule?.suitLabel?.[parsedMinor.suitId] || toTitleCase(parsedMinor.suitId));
|
|
const template = String(minorRule.template || "{number}_{rank} {suit}.webp");
|
|
|
|
return applyTemplate(template, {
|
|
number: cardNumber,
|
|
rank: parsedMinor.rankWord,
|
|
rankKey: parsedMinor.rankKey,
|
|
suit: suitWord,
|
|
suitId: parsedMinor.suitId,
|
|
index: rankIndex + 1
|
|
});
|
|
}
|
|
|
|
if (minorRule.mode === "suit-prefix-and-rank-order") {
|
|
const suitPrefix = minorRule?.suitPrefix?.[parsedMinor.suitId];
|
|
if (!suitPrefix) {
|
|
return null;
|
|
}
|
|
|
|
const indexStart = Number.isInteger(minorRule.indexStart) ? minorRule.indexStart : 1;
|
|
const indexPad = Number.isInteger(minorRule.indexPad) ? minorRule.indexPad : 2;
|
|
const suitIndex = String(indexStart + rankIndex).padStart(indexPad, "0");
|
|
const template = String(minorRule.template || "{suit}{index}.png");
|
|
|
|
return applyTemplate(template, {
|
|
suit: suitPrefix,
|
|
suitId: parsedMinor.suitId,
|
|
index: suitIndex,
|
|
rank: parsedMinor.rankWord,
|
|
rankKey: parsedMinor.rankKey
|
|
});
|
|
}
|
|
|
|
if (minorRule.mode === "suit-base-number-template") {
|
|
const suitBaseRaw = Number(minorRule?.suitBase?.[parsedMinor.suitId]);
|
|
if (!Number.isFinite(suitBaseRaw)) {
|
|
return null;
|
|
}
|
|
|
|
const numberPad = Number.isInteger(minorRule.numberPad) ? minorRule.numberPad : 2;
|
|
const cardNumber = String(suitBaseRaw + rankIndex).padStart(numberPad, "0");
|
|
const template = String(minorRule.template || "{number}.png");
|
|
|
|
return applyTemplate(template, {
|
|
number: cardNumber,
|
|
suitId: parsedMinor.suitId,
|
|
rank: parsedMinor.rankWord,
|
|
rankKey: parsedMinor.rankKey,
|
|
index: rankIndex
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveCardRelativePath(manifest, cardName) {
|
|
if (!manifest) {
|
|
return null;
|
|
}
|
|
|
|
const canonical = canonicalMajorName(cardName);
|
|
const majorFile = resolveMajorFile(manifest, canonical);
|
|
if (majorFile) {
|
|
return majorFile;
|
|
}
|
|
|
|
const parsedMinor = parseMinorCard(cardName);
|
|
if (!parsedMinor) {
|
|
return null;
|
|
}
|
|
|
|
return resolveMinorFile(manifest, parsedMinor);
|
|
}
|
|
|
|
function resolveWithDeck(deckId, cardName, variant = "full") {
|
|
const manifest = getDeckManifest(deckId);
|
|
if (!manifest) {
|
|
return null;
|
|
}
|
|
|
|
const relativePath = resolveCardRelativePath(manifest, cardName);
|
|
if (!relativePath) {
|
|
return null;
|
|
}
|
|
|
|
if (variant === "thumbnail") {
|
|
return resolveDeckThumbnailPath(manifest, relativePath) || `${manifest.basePath}/${relativePath}`;
|
|
}
|
|
|
|
return `${manifest.basePath}/${relativePath}`;
|
|
}
|
|
|
|
function resolveTarotCardImage(cardName, optionsOrDeckId) {
|
|
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
|
const activePath = resolveWithDeck(resolvedDeckId, cardName);
|
|
if (activePath) {
|
|
return encodeURI(activePath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveTarotCardThumbnail(cardName, optionsOrDeckId) {
|
|
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
|
const thumbnailPath = resolveWithDeck(resolvedDeckId, cardName, "thumbnail");
|
|
if (thumbnailPath) {
|
|
return encodeURI(thumbnailPath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveTarotCardBackImage(optionsOrDeckId) {
|
|
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
|
|
|
if (cardBackCache.has(resolvedDeckId)) {
|
|
const cachedPath = cardBackCache.get(resolvedDeckId);
|
|
return cachedPath ? encodeURI(cachedPath) : null;
|
|
}
|
|
|
|
const manifest = getDeckManifest(resolvedDeckId);
|
|
const activeBackPath = resolveDeckCardBackPath(manifest);
|
|
cardBackCache.set(resolvedDeckId, activeBackPath || null);
|
|
|
|
if (activeBackPath) {
|
|
return encodeURI(activeBackPath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveTarotCardBackThumbnail(optionsOrDeckId) {
|
|
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
|
|
|
if (cardBackThumbnailCache.has(resolvedDeckId)) {
|
|
const cachedPath = cardBackThumbnailCache.get(resolvedDeckId);
|
|
return cachedPath ? encodeURI(cachedPath) : null;
|
|
}
|
|
|
|
const manifest = getDeckManifest(resolvedDeckId);
|
|
const relativeBackPath = String(manifest?.cardBack || manifest?.cardBackPath || "").trim();
|
|
const thumbnailPath = resolveDeckThumbnailPath(manifest, relativeBackPath) || resolveDeckCardBackPath(manifest);
|
|
cardBackThumbnailCache.set(resolvedDeckId, thumbnailPath || null);
|
|
|
|
return thumbnailPath ? encodeURI(thumbnailPath) : null;
|
|
}
|
|
|
|
function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) {
|
|
const manifest = getDeckManifest(deckId);
|
|
const fallbackName = String(cardName || "").trim();
|
|
if (!manifest) {
|
|
return fallbackName;
|
|
}
|
|
|
|
let resolvedTrumpNumber = normalizeTrumpNumber(trumpNumber);
|
|
if (!Number.isInteger(resolvedTrumpNumber)) {
|
|
const canonical = canonicalMajorName(cardName);
|
|
resolvedTrumpNumber = normalizeTrumpNumber(trumpNumberByCanonicalName[canonical]);
|
|
}
|
|
|
|
if (Number.isInteger(resolvedTrumpNumber)) {
|
|
const byTrump = manifest?.majorNameOverridesByTrump?.[resolvedTrumpNumber];
|
|
if (byTrump) {
|
|
return byTrump;
|
|
}
|
|
}
|
|
|
|
const canonical = canonicalMajorName(cardName);
|
|
const override = manifest?.nameOverrides?.[canonical];
|
|
if (override) {
|
|
return override;
|
|
}
|
|
|
|
const minorKey = canonicalMinorName(cardName);
|
|
const minorOverride = manifest?.minorNameOverrides?.[minorKey];
|
|
if (minorOverride) {
|
|
return minorOverride;
|
|
}
|
|
|
|
return fallbackName;
|
|
}
|
|
|
|
function getTarotCardSearchAliases(cardName, optionsOrDeckId) {
|
|
const fallbackName = String(cardName || "").trim();
|
|
if (!fallbackName) {
|
|
return [];
|
|
}
|
|
|
|
const { resolvedDeckId, trumpNumber } = resolveDeckOptions(optionsOrDeckId);
|
|
const aliases = new Set();
|
|
aliases.add(fallbackName);
|
|
|
|
const displayName = String(resolveDisplayNameWithDeck(resolvedDeckId, fallbackName, trumpNumber) || "").trim();
|
|
if (displayName) {
|
|
aliases.add(displayName);
|
|
}
|
|
|
|
const canonicalMajor = canonicalMajorName(fallbackName);
|
|
const resolvedTrumpNumber = Number.isInteger(normalizeTrumpNumber(trumpNumber))
|
|
? normalizeTrumpNumber(trumpNumber)
|
|
: normalizeTrumpNumber(trumpNumberByCanonicalName[canonicalMajor]);
|
|
|
|
if (Number.isInteger(resolvedTrumpNumber)) {
|
|
aliases.add(canonicalMajor);
|
|
aliases.add(`the ${canonicalMajor}`);
|
|
aliases.add(`trump ${resolvedTrumpNumber}`);
|
|
}
|
|
|
|
const parsedMinor = parseMinorCard(fallbackName);
|
|
if (parsedMinor) {
|
|
const suitAliases = suitSearchAliasesById[parsedMinor.suitId] || [parsedMinor.suitId];
|
|
suitAliases.forEach((suitAlias) => {
|
|
aliases.add(`${parsedMinor.rankKey} of ${suitAlias}`);
|
|
if (Number.isInteger(parsedMinor.pipValue)) {
|
|
aliases.add(`${parsedMinor.pipValue} of ${suitAlias}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
return Array.from(aliases);
|
|
}
|
|
|
|
function getTarotCardDisplayName(cardName, optionsOrDeckId) {
|
|
const { resolvedDeckId, trumpNumber } = resolveDeckOptions(optionsOrDeckId);
|
|
|
|
return resolveDisplayNameWithDeck(resolvedDeckId, cardName, trumpNumber);
|
|
}
|
|
|
|
function setActiveDeck(deckId) {
|
|
activeDeckId = normalizeDeckId(deckId);
|
|
getDeckManifest(activeDeckId);
|
|
return activeDeckId;
|
|
}
|
|
|
|
function getDeckOptions() {
|
|
return Object.values(getDeckManifestSources()).map((source) => {
|
|
const manifest = getDeckManifest(source.id);
|
|
return {
|
|
id: source.id,
|
|
label: manifest?.label || source.label
|
|
};
|
|
});
|
|
}
|
|
|
|
document.addEventListener("settings:updated", (event) => {
|
|
const nextDeck = event?.detail?.settings?.tarotDeck;
|
|
setActiveDeck(nextDeck);
|
|
});
|
|
|
|
window.TarotCardImages = {
|
|
resolveTarotCardImage,
|
|
resolveTarotCardThumbnail,
|
|
resolveTarotCardBackImage,
|
|
resolveTarotCardBackThumbnail,
|
|
getTarotCardDisplayName,
|
|
getTarotCardSearchAliases,
|
|
setActiveDeck,
|
|
getDeckOptions,
|
|
getActiveDeck: () => activeDeckId
|
|
};
|
|
})();
|