var FamiliarArcanaResolve = FamiliarArcanaResolve || (function () { 'use strict'; var version = '1.2'; function esc(value) { return String(value || '').replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function replaceInlineRolls(content, msg) { if (!msg.inlinerolls) return content; return String(content || '').replace(/\$\[\[(\d+)\]\]/g, function (match, index) { var roll = msg.inlinerolls[parseInt(index, 10)]; return roll && roll.results ? roll.results.total : match; }); } function parseArgs(content) { var args = {}; var re = /--([a-zA-Z0-9_-]+)(?:\s+("[^"]*"|'[^']*'|[^\s]+))?/g; var match; while ((match = re.exec(content)) !== null) { var key = match[1].toLowerCase(); var value = match[2] || '1'; if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) { value = value.substring(1, value.length - 1); } args[key] = value; } return args; } function numberFrom(value, fallback) { var match = String(value || '').match(/[-+]?\d+(\.\d+)?/); if (!match) return fallback; var number = parseFloat(match[0]); return isNaN(number) ? fallback : number; } function findAttr(characterId, names) { for (var i = 0; i < names.length; i++) { var attr = findObjs({ type: 'attribute', characterid: characterId, name: names[i] })[0]; if (attr) return attr; } return null; } function getAttrText(characterId, names) { var attr = findAttr(characterId, names); return attr ? String(attr.get('current') || '') : ''; } function getAttrNumber(characterId, names, fallback) { return numberFrom(getAttrText(characterId, names), fallback); } function resolveRollValue(value, callback) { value = String(value || '').trim(); if (/^[-+]?\d+(\.\d+)?$/.test(value)) { callback(parseFloat(value)); return; } if (value.indexOf('[[') === 0 && value.lastIndexOf(']]') === value.length - 2) { value = value.slice(2, -2).trim(); } sendChat('Familiar Arcana', '[[' + value + ']]', function (ops) { var total = null; if (ops && ops[0] && ops[0].inlinerolls && ops[0].inlinerolls[0]) { total = ops[0].inlinerolls[0].results.total; } callback(total); }); } function selectedTokens(msg) { if (!msg.selected || !msg.selected.length) return []; var tokens = []; msg.selected.forEach(function (selected) { var token = getObj('graphic', selected._id); if (token) tokens.push(token); }); return tokens; } function getCharacterForToken(token) { var characterId = token.get('represents'); return characterId ? getObj('character', characterId) : null; } function getPb(characterId) { return getAttrNumber(characterId, [ 'npc_pb', 'pb', 'npc_proficiency_bonus', 'proficiency_bonus' ], 0); } function getAbilityMod(characterId, ability) { var map = { strength: ['npc_strength_mod', 'npc_str_mod', 'strength_mod', 'str_mod'], dexterity: ['npc_dexterity_mod', 'npc_dex_mod', 'dexterity_mod', 'dex_mod'], constitution: ['npc_constitution_mod', 'npc_con_mod', 'constitution_mod', 'con_mod'], intelligence: ['npc_intelligence_mod', 'npc_int_mod', 'intelligence_mod', 'int_mod'], wisdom: ['npc_wisdom_mod', 'npc_wis_mod', 'wisdom_mod', 'wis_mod'], charisma: ['npc_charisma_mod', 'npc_cha_mod', 'charisma_mod', 'cha_mod'] }; return getAttrNumber(characterId, map[ability] || [], 0); } function getDefense(characterId, defenseName) { var key = String(defenseName || 'AC').toLowerCase(); var pb = getPb(characterId); if (key === 'ac' || key === 'armor' || key === 'armorclass') { return { label: 'AC', value: getAttrNumber(characterId, ['npc_ac', 'npc_acbase', 'ac'], 10) }; } if (key === 'reflex' || key === 'ref') { return { label: 'Reflex', value: 10 + pb + Math.max( getAbilityMod(characterId, 'strength'), getAbilityMod(characterId, 'dexterity') ) }; } if (key === 'cognitive' || key === 'cog') { return { label: 'Cognitive', value: 10 + pb + Math.max( getAbilityMod(characterId, 'intelligence'), getAbilityMod(characterId, 'wisdom') ) }; } if (key === 'will' || key === 'willpower') { return { label: 'Will', value: 10 + pb + Math.max( getAbilityMod(characterId, 'constitution'), getAbilityMod(characterId, 'charisma') ) }; } return { label: 'AC', value: getAttrNumber(characterId, ['npc_ac', 'npc_acbase', 'ac'], 10) }; } function normalizeDamageType(value) { return String(value || '') .toLowerCase() .replace(/damage/g, '') .replace(/[^a-z]/g, ''); } function traitText(characterId, kind) { var names = { resistance: ['npc_resistances', 'npc_resistance', 'resistances', 'resistance', 'damage_resistances'], immunity: ['npc_immunities', 'npc_immunity', 'immunities', 'immunity', 'damage_immunities'], vulnerability: ['npc_vulnerabilities', 'npc_vulnerability', 'vulnerabilities', 'vulnerability', 'damage_vulnerabilities'] }; var parts = []; names[kind].forEach(function (name) { var text = getAttrText(characterId, [name]); if (text) parts.push(text); }); return parts.join(' ').toLowerCase(); } function traitMatches(text, damageType, magical) { var type = normalizeDamageType(damageType); var clean = normalizeDamageType(text); if (!type || !clean) return false; if (clean.indexOf('nonmagical') !== -1 && magical) return false; if (clean.indexOf(type) !== -1) return true; if (clean.indexOf('all') !== -1) return true; if ((type === 'acid' || type === 'cold' || type === 'fire' || type === 'lightning' || type === 'thunder') && clean.indexOf('elemental') !== -1) { return true; } return false; } function applyDamageTraits(characterId, damage, damageType, magical) { var result = { damage: Math.max(0, Math.floor(damage)), immune: false, resistant: false, vulnerable: false }; if (result.damage <= 0) return result; if (traitMatches(traitText(characterId, 'immunity'), damageType, magical)) { result.damage = 0; result.immune = true; return result; } if (traitMatches(traitText(characterId, 'resistance'), damageType, magical)) { result.damage = Math.floor(result.damage / 2); result.resistant = true; } if (traitMatches(traitText(characterId, 'vulnerability'), damageType, magical)) { result.damage = result.damage * 2; result.vulnerable = true; } return result; } function getOutcome(attackTotal, defenseValue, baseDamage) { if (attackTotal >= defenseValue) { return { label: 'Full Hit', damage: Math.floor(baseDamage) }; } if (attackTotal >= defenseValue / 2) { return { label: 'Partial Hit', damage: Math.floor(baseDamage / 2) }; } return { label: 'Miss', damage: 0 }; } function getTempAttr(characterId) { return findAttr(characterId, ['hp_temp', 'temp_hp', 'temporary_hp', 'temphp']); } function getHpSource(character, token, hpBar) { var barValue = token.get('bar' + hpBar + '_value'); if (String(barValue || '').trim() !== '') { return { type: 'bar', current: numberFrom(barValue, 0) }; } var hpAttr = findAttr(character.id, ['hp', 'npc_hp', 'hit_points']); if (hpAttr) { return { type: 'attr', attr: hpAttr, current: numberFrom(hpAttr.get('current'), 0) }; } return null; } function applyDamage(character, token, hpBar, damage) { var result = { oldHp: null, newHp: null, tempUsed: 0, hpDamage: 0, changed: false, missingHp: false }; damage = Math.max(0, Math.floor(damage)); if (damage <= 0) { var unchanged = getHpSource(character, token, hpBar); if (unchanged) { result.oldHp = unchanged.current; result.newHp = unchanged.current; } return result; } var tempAttr = getTempAttr(character.id); var tempHp = tempAttr ? numberFrom(tempAttr.get('current'), 0) : 0; if (tempHp > 0) { result.tempUsed = Math.min(tempHp, damage); damage -= result.tempUsed; tempAttr.setWithWorker({ current: tempHp - result.tempUsed }); } var hp = getHpSource(character, token, hpBar); if (!hp) { result.missingHp = true; return result; } result.oldHp = hp.current; result.hpDamage = damage; result.newHp = Math.max(0, hp.current - damage); if (hp.type === 'bar') { token.set('bar' + hpBar + '_value', result.newHp); } else { hp.attr.setWithWorker({ current: result.newHp }); } result.changed = true; return result; } function buildResolveLine(tokenName, defense, outcome) { return ( '