var FamiliarArcana = FamiliarArcana || (function () { 'use strict'; var version = '2.9'; function rowId() { var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; var id = '-'; for (var i = 0; i < 19; i++) id += chars.charAt(Math.floor(Math.random() * chars.length)); return id; } function getCharacterFromSelection(msg) { if (!msg.selected || !msg.selected.length) { sendChat('Familiar Arcana', '/w gm Select a token first.'); return null; } var token = getObj('graphic', msg.selected[0]._id); if (!token) return null; var characterId = token.get('represents'); if (!characterId) { sendChat('Familiar Arcana', '/w gm Selected token does not represent a character.'); return null; } return getObj('character', characterId); } function getAttrCurrent(characterId, name) { var attr = findObjs({ type: 'attribute', characterid: characterId, name: name })[0]; return attr ? String(attr.get('current') || '') : ''; } function getSpellModAttr(characterId, override) { override = String(override || '').toLowerCase(); if (override === 'strength_mod') return 'strength_mod'; if (override === 'dexterity_mod') return 'dexterity_mod'; if (override === 'constitution_mod') return 'constitution_mod'; if (override === 'intelligence_mod') return 'intelligence_mod'; if (override === 'wisdom_mod') return 'wisdom_mod'; if (override === 'charisma_mod') return 'charisma_mod'; var ability = getAttrCurrent(characterId, 'spellcasting_ability').toLowerCase(); if (ability.indexOf('charisma') !== -1 || ability.indexOf('cha') !== -1) return 'charisma_mod'; if (ability.indexOf('intelligence') !== -1 || ability.indexOf('int') !== -1) return 'intelligence_mod'; if (ability.indexOf('wisdom') !== -1 || ability.indexOf('wis') !== -1) return 'wisdom_mod'; if (ability.indexOf('constitution') !== -1 || ability.indexOf('con') !== -1) return 'constitution_mod'; return 'charisma_mod'; } function replaceMacroMods(text, characterId, override) { var spellModAttr = getSpellModAttr(characterId, override); return String(text || '') .replace(/@\{spell_mod\}/g, '@{' + spellModAttr + '}') .replace(/@\{fighting_mod\}/g, '@{strength_mod}') .replace(/@\{qs_mod\}/g, '@{dexterity_mod}') .replace(/@\{con_mod\}/g, '@{constitution_mod}') .replace(/spell_mod/g, '@{' + spellModAttr + '}') .replace(/fighting_mod/g, '@{strength_mod}') .replace(/qs_mod/g, '@{dexterity_mod}') .replace(/con_mod/g, '@{constitution_mod}'); } function hasCantripShortcut(value) { return /cantrip-d(4|6|8|10|12|20)/i.test(String(value || '')); } function isCantripLevel(level) { level = String(level || '0').toLowerCase(); return level === '0' || level === 'cantrip'; } function normalizeDamageFormula(value) { value = String(value || '').trim(); if (value.indexOf('[[') === 0 && value.lastIndexOf(']]') === value.length - 2) { return value.slice(2, -2).trim(); } return value; } function expandDiceShortcut(value, characterId, override) { value = String(value || '').trim(); value = value.replace(/cantrip-d(4|6|8|10|12|20)/gi, function (match, die) { return '1d' + die; }); return replaceMacroMods(value, characterId, override); } function shouldUseCantripScaling(args, rawMacro, level, damage) { var flag = String(args.cantrip || args.scale || args.progression || '').toLowerCase(); var key = String(rawMacro || '').trim().toLowerCase(); if (flag === '1' || flag === 'yes' || flag === 'true' || flag === 'cantrip' || flag === 'cantrip dice') return true; if (hasCantripShortcut(rawMacro)) return true; return isCantripLevel(level) && String(damage || '').trim() !== '' && key !== 'mm'; } function cantripAttackDamage(damage) { return String(damage || '').replace(/^1d(4|6|8|10|12|20)\b/i, function (match, die) { return '((@{level} + 1) / 6 + 0.5)d(' + die + ')'; }); } function specialDescription(key, description, characterId, override) { key = String(key || '').trim().toLowerCase(); if (key === 'mm') { return replaceMacroMods( "You create [[spell_mod]] glowing missiles, with a minimum of three missiles. Before rolling damage, assign each missile to one or more creatures you can see within range.\n\n" + "Each missile automatically hits and deals [[1d4 + 1]] Force damage. Roll the damage once and use that result for every missile. All missiles strike simultaneously.", characterId, override ); } if (key === 'scorch') { return replaceMacroMods( "You create [[spell_mod]] searing rays. You can direct the rays at one creature or several.\n\n" + "For each ray, make a ranged spell attack against the target's AC. On a full hit, each ray deals Fire damage, halved against the same target.", characterId, override ); } return replaceMacroMods(description, characterId, override); } function setAttr(characterId, name, value) { var attr = findObjs({ type: 'attribute', characterid: characterId, name: name })[0]; if (attr) { attr.setWithWorker({ current: value }); } else { attr = createObj('attribute', { characterid: characterId, name: name, current: '' }); attr.setWithWorker({ current: value }); } } 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 spellSection(level) { level = String(level || '0').toLowerCase(); return level === '0' || level === 'cantrip' ? 'repeating_spell-cantrip' : 'repeating_spell-' + level; } function cleanAttackType(value) { value = String(value || 'None').toLowerCase(); if (value === 'melee') return 'Melee'; if (value === 'ranged') return 'Ranged'; return 'None'; } function schoolFromAspect(aspect, fallbackSchool) { if (fallbackSchool && String(fallbackSchool).toLowerCase() !== 'all') return fallbackSchool; aspect = String(aspect || '').toLowerCase(); if (aspect === 'passion') return 'Evocation'; if (aspect === 'virtue') return 'Abjuration'; if (aspect === 'corruption') return 'Illusion'; if (aspect === 'growth') return 'Transmutation'; return 'Evocation'; } function buildDamage(args, characterId) { var diceMacro = args.dice || args.macro || args.damage || ''; var key = String(diceMacro).trim().toLowerCase(); if (key === 'mm') return ''; if (key === 'scorch') return '2d6'; return normalizeDamageFormula(expandDiceShortcut(diceMacro, characterId, args.spellmod)); } function getRowIdFromAttrName(name) { var match = name.match(/^repeating_attack_([^_]+)_/); return match ? match[1] : null; } function repairGeneratedAttack(characterId, data) { var attackNames = findObjs({ type: 'attribute', characterid: characterId }).filter(function (attr) { return /^repeating_attack_[^_]+_atkname$/.test(attr.get('name')); }); var targetRow = null; attackNames.forEach(function (attr) { var current = String(attr.get('current') || ''); if (current === data.name || current === '' || data.name.indexOf(current) !== -1 || current.indexOf(data.name) !== -1) { targetRow = getRowIdFromAttrName(attr.get('name')); } }); if (!targetRow && attackNames.length) targetRow = getRowIdFromAttrName(attackNames[attackNames.length - 1].get('name')); if (!targetRow) return; var prefix = 'repeating_attack_' + targetRow + '_'; var attackDamage = data.isCantrip ? cantripAttackDamage(data.damage) : data.damage; setAttr(characterId, prefix + 'atkname', data.name); setAttr(characterId, prefix + 'atkflag', '{{attack=1}}'); setAttr(characterId, prefix + 'atkattr_base', 'spell'); setAttr(characterId, prefix + 'atkprofflag', '(@{pb})'); setAttr(characterId, prefix + 'atkmod', '0'); setAttr(characterId, prefix + 'atkrange', data.range); setAttr(characterId, prefix + 'dmgbase', attackDamage); setAttr(characterId, prefix + 'dmgtype', data.damageType); setAttr(characterId, prefix + 'dmgattr', '0'); setAttr(characterId, prefix + 'dmgattr_base', '0'); setAttr(characterId, prefix + 'dmgmod', '0'); setAttr(characterId, prefix + 'crit1', attackDamage); setAttr(characterId, prefix + 'dmg2base', ''); setAttr(characterId, prefix + 'dmg2type', ''); setAttr(characterId, prefix + 'dmg2attr', '0'); setAttr(characterId, prefix + 'dmg2attr_base', '0'); setAttr(characterId, prefix + 'dmg2mod', ''); setAttr(characterId, prefix + 'crit2', ''); setAttr(characterId, prefix + 'atk_desc', data.description); setAttr(characterId, prefix + 'saveflag', '0'); setAttr(characterId, prefix + 'saveattr', ''); setAttr(characterId, prefix + 'savedc', ''); setAttr(characterId, prefix + 'saveeffect', ''); } function createSpell(msg) { var character = getCharacterFromSelection(msg); if (!character) return; var args = parseArgs(msg.content); var characterId = character.id; var rawMacro = args.dice || args.macro || args.damage || ''; var macroKey = String(rawMacro || '').trim().toLowerCase(); var name = args.name || 'New Spell'; var level = args.level || '0'; var school = schoolFromAspect(args.aspect, args.school); var castingTime = args.time || '1 Action'; var range = args.range || 'Self'; var duration = args.duration || 'Instantaneous'; var target = args.target || ''; var attackType = macroKey === 'mm' ? 'None' : cleanAttackType(args.attack || args.type || 'None'); var damage = buildDamage(args, characterId); var isCantripMacro = shouldUseCantripScaling(args, rawMacro, level, damage); var damageType = args.dtype || args.damagetype || ''; var description = specialDescription(macroKey, args.description || args.desc || '', characterId, args.spellmod); var concentration = args.concentration === '1' || String(args.concentration).toLowerCase() === 'yes' ? '{{concentration=1}}' : '0'; var ritual = args.ritual === '1' || String(args.ritual).toLowerCase() === 'yes' ? '{{ritual=1}}' : '0'; var verbal = args.v === '0' || String(args.verbal).toLowerCase() === 'no' ? '0' : '{{v=1}}'; var somatic = args.s === '0' || String(args.somatic).toLowerCase() === 'no' ? '0' : '{{s=1}}'; var material = args.m || args.material ? '{{m=' + (args.m || args.material) + '}}' : '{{m=0}}'; var spellRow = rowId(); var prefix = spellSection(level) + '_' + spellRow + '_'; setAttr(characterId, prefix + 'spellschool', school); setAttr(characterId, prefix + 'spellcastingtime', castingTime); setAttr(characterId, prefix + 'spellrange', range); setAttr(characterId, prefix + 'spell_ability', '@{spellcasting_ability}+'); setAttr(characterId, prefix + 'spellcomp_v', verbal); setAttr(characterId, prefix + 'spellcomp_s', somatic); setAttr(characterId, prefix + 'spellcomp_m', material); setAttr(characterId, prefix + 'spellconcentration', concentration); setAttr(characterId, prefix + 'spellritual', ritual); setAttr(characterId, prefix + 'spellduration', duration); setAttr(characterId, prefix + 'spelloutput', attackType === 'None' ? 'SPELLCARD' : 'ATTACK'); setAttr(characterId, prefix + 'spellattack', attackType); setAttr(characterId, prefix + 'spell_damage_progression', isCantripMacro ? 'Cantrip Dice' : ''); setAttr(characterId, prefix + 'spelldamage', damage); setAttr(characterId, prefix + 'spelldamagetype', damageType); setAttr(characterId, prefix + 'spelltarget', target); setAttr(characterId, prefix + 'spellsave', ''); setAttr(characterId, prefix + 'spellsavedc', ''); setAttr(characterId, prefix + 'spellsavestat', ''); setAttr(characterId, prefix + 'spelldescription', description); setAttr(characterId, prefix + 'spellname', name); if (attackType !== 'None') { setTimeout(function () { repairGeneratedAttack(characterId, { name: name, range: range, damage: damage, damageType: damageType, description: description, isCantrip: isCantripMacro }); }, 1200); } sendChat('Familiar Arcana', '/w gm Created ' + name + ' on ' + character.get('name') + '.'); } function handleInput(msg) { if (msg.type !== 'api') return; if (msg.content.indexOf('!fa-spell') === 0) createSpell(msg); } on('chat:message', handleInput); on('ready', function () { log('Familiar Arcana API v' + version + ' ready.'); }); return {}; }());