export const DAY_SECTIONS = { arte: ['Actuar', 'Cantar', 'Poesía', 'Dibujo', 'Esculpir', 'Tocar', 'Bailar'], persuasion: ['Elocuencia', 'Mando', 'Psicología', 'Protocolo', 'Seducción', 'Estilismo'], conocimientos: ['Tecnología', 'Biología', 'Rural', 'Historia', 'Magia', 'Lenguas'], artesania: ['Confección', 'Orfebrería', 'Cocinar', 'Ebanistería', 'Herrería', 'Valorar'] }; export const DAY_LABELS = { arte: 'Arte', persuasion: 'Persuasión', conocimientos: 'Conocimientos', artesania: 'Artesanía' }; export const NIGHT_SECTIONS = { combate: ['Ofensivo', 'Defensivo', 'Táctica'], magia: ['Ofensivo', 'Defensivo', 'Ritual'], fe: ['Ofensivo', 'Defensivo', 'Sanación'], subterfugio: ['Huir', 'Esconderse', 'Descubrir', 'Manipular'] }; export const NIGHT_LABELS = { combate: 'Combate', magia: 'Magia', fe: 'Fe', subterfugio: 'Subterfugio' }; export const DAMAGE_LABELS = { combat: 'combate', magic: 'magia', faith: 'fe' }; export const norm = s => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); export const slug = s => norm(s).replace(/\s+/g, '-') || 'item'; export const esc = (v = '') => String(v ?? '').replace(/[&<>\"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); export const number = v => { const n = Number(v || 0); return Number.isFinite(n) ? n : 0; }; export const rnd = (min, max) => Math.floor(Number(min) + Math.random() * (Number(max) - Number(min) + 1)); export const choice = arr => arr[Math.floor(Math.random() * arr.length)]; export function weightedChoice(items, weights = {}) { const arr = (items || []).filter(Boolean); if (!arr.length) return null; const normalized = arr.map(x => ({ item: x, weight: Math.max(0, number(weights?.[x] ?? weights?.[norm(x)] ?? 0)) })); const total = normalized.reduce((a, x) => a + x.weight, 0); if (total <= 0) return choice(arr); let roll = Math.random() * total; for (const x of normalized) { roll -= x.weight; if (roll <= 0) return x.item; } return normalized[normalized.length - 1].item; } export function normalizeWeights(weights = {}, allowed = []) { const out = {}; for (const k of allowed) out[k] = Math.max(0, number(weights?.[k] ?? weights?.[norm(k)] ?? 0)); return out; } export function cleanCategory(c) { const v = String(c || 'Normal'); if (/plata/i.test(v)) return 'Plata'; if (/oro/i.test(v)) return 'Oro'; if (/especial/i.test(v)) return 'Especial'; return 'Normal'; } export function cardBaseId(c = {}) { return c.baseId || c.variantOf || slug(c.baseName || c.name || c.id); } export function normalizeCard(c = {}) { const baseId = cardBaseId(c); const category = cleanCategory(c.category || c.variantLabel || 'Normal'); return { ...c, baseId, category, baseName: c.baseName || c.name || baseId, groups: Array.isArray(c.groups) ? c.groups : [] }; } export function bestOwnedCards(cards, collection) { const rank = { Normal: 1, Plata: 2, Oro: 3, Especial: 4 }; const out = new Map(); for (const raw of cards.map(normalizeCard)) { const qty = number(collection?.[raw.id]?.qty || collection?.[raw.id] || 0); if (qty <= 0) continue; const prev = out.get(raw.baseId); if (!prev || (rank[raw.category] || 0) > (rank[prev.category] || 0)) out.set(raw.baseId, raw); } return [...out.values()].sort((a, b) => String(a.name).localeCompare(String(b.name))); } export function resolveImage(card = {}, selectedSkins = {}, skinMap = {}) { const skin = skinMap[selectedSkins[card.baseId]]; return skin?.image || card.image || card.imageVariants?.[0]?.image || ''; } export function cardSkill(card = {}, section, skill) { return number(card.day?.[section]?.[skill] ?? card.night?.[section]?.[skill] ?? card.night?.base?.[skill]); } export function characterHp(card = {}) { return Math.max(1, number(card.night?.base?.PV || 1)); } export function passesRestriction(card = {}, restrictions = []) { if (!restrictions || !restrictions.length) return true; const groups = (card.groups || []).map(norm); return restrictions.every(r => { if (!r) return true; if (typeof r === 'string') return groups.includes(norm(r)); if (r.type === 'onlyGroup') return groups.includes(norm(r.group)); if (r.type === 'requireGroup') return groups.includes(norm(r.group)); return true; }); } export function makeDayChallengeCards(challenge, count = 4) { const scripted = Array.isArray(challenge.scriptedCards) ? challenge.scriptedCards : []; if (scripted.length) { return Array.from({ length: count }, (_, i) => { const src = scripted[i % scripted.length] || {}; const section = src.section || 'arte'; const skills = DAY_SECTIONS[section] || []; const skill = src.skill || choice(skills); return { id: `d${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`, type: 'dia', section, sectionLabel: DAY_LABELS[section] || section, skill, target: number(src.target ?? 1), revealed: false, resolved: false, used: [], tutorial: src.tutorial || '' }; }); } const cats = (challenge.categories && challenge.categories.length) ? challenge.categories : Object.keys(DAY_SECTIONS); const min = number(challenge.difficulty?.min ?? 1); const max = number(challenge.difficulty?.max ?? 6); return Array.from({ length: count }, (_, i) => { const section = choice(cats); const skills = DAY_SECTIONS[section] || []; return { id: `d${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`, type: 'dia', section, sectionLabel: DAY_LABELS[section] || section, skill: choice(skills), target: rnd(min, max), revealed: false, resolved: false, used: [] }; }); } export function makeNightChallengeCards(challenge, count = 4) { const scripted = Array.isArray(challenge.scriptedCards) ? challenge.scriptedCards : []; if (scripted.length) { return Array.from({ length: count }, (_, i) => { const src = scripted[i % scripted.length] || {}; const section = src.section || 'combate'; const skill = src.skill || choice(NIGHT_SECTIONS[section] || []); return { id: `n${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`, type: 'noche', section, sectionLabel: NIGHT_LABELS[section] || section, skill, target: number(src.target ?? 1), revealed: false, resolved: false, used: [], tutorial: src.tutorial || '' }; }); } const cats = Object.keys(NIGHT_SECTIONS); const enemy = challenge.enemy || {}; const cardWeights = normalizeWeights(challenge.cardWeights || challenge.categoryWeights || {}, cats); const skillWeights = challenge.skillWeights || {}; return Array.from({ length: count }, (_, i) => { const section = weightedChoice(cats, cardWeights) || choice(cats); const skills = NIGHT_SECTIONS[section] || []; const sectionSkillWeights = normalizeWeights(skillWeights?.[section] || {}, skills); const skill = weightedChoice(skills, sectionSkillWeights) || choice(skills); const target = number(enemy.difficulties?.[section]?.[skill] ?? 1); return { id: `n${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`, type: 'noche', section, sectionLabel: NIGHT_LABELS[section] || section, skill, target, revealed: false, resolved: false, used: [] }; }); } export function newNightEnemy(challenge) { const e = challenge.enemy || {}; return { name: e.name || 'Enemigo', image: e.image || '', normalPv: number(e.normalPv || 1), strangePv: number(e.strangePv || 0), damage: { combat: number(e.damage?.combat || 1), magic: number(e.damage?.magic || 1), faith: number(e.damage?.faith || 1) }, discoverToWin: number(e.discoverToWin || 0), discovery: 0, maxTurns: number(e.maxTurns || 0) }; } export function isNightWin(enemy) { return number(enemy.normalPv) <= 0 && number(enemy.strangePv) <= 0 && number(enemy.discovery) >= number(enemy.discoverToWin); } export function pickDamageKind(enemy) { return choice(['combat', 'magic', 'faith']); }