Files
roll20-character-sheets/Pathfinder Community/dev/src/PFAbility.js
T

1285 lines
45 KiB
JavaScript

import _ from 'underscore';
import {PFLog, PFConsole} from './PFLog';
import TAS from './TheAaronSheet.js';
import * as SWUtils from './SWUtils';
import PFConst from './PFConst';
import * as PFUtils from './PFUtils';
import * as PFUtilsAsync from './PFUtilsAsync';
import * as PFMacros from './PFMacros';
import * as PFMenus from './PFMenus';
import * as PFDB from './PFDB';
import * as PFAttackOptions from './PFAttackOptions';
import * as PFAttackGrid from './PFAttackGrid';
import * as PFFeatures from './PFFeatures';
import * as PFAttacks from './PFAttacks';
import * as PFAbility from './PFAbility';
let optionFields = ['is_sp', 'hasposrange', 'hasuses', 'hasattack', 'abil-attacktypestr'],
optionRepeatingHelperFields = ['ability_type', 'range_numeric', 'frequency', 'abil-attack-type'],
allOptionRepeatingFields = optionFields.concat(optionRepeatingHelperFields),
tabRuleSorted = {
'class-features': 0,
feats: 1,
'monster-rule': 8,
'mythic-abilities': 3,
'mythic-feats': 1,
other: 8,
'racial-traits': 2,
'special-abilities': 5,
'special-attacks': 4,
'special-qualities': 7,
'spell-like-abilities': 6,
traits: 2,
},
tabTypeSorted = {
Ex: 9,
Sp: 10,
Su: 11,
},
categoryAttrs = ['tabcat-1', 'tabcat0', 'tabcat1', 'tabcat2', 'tabcat3', 'tabcat4', 'tabcat5', 'tabcat6', 'tabcat7', 'tabcat8', 'tabcat9', 'tabcat10', 'tabcat11'],
otherCommandMacros = {
ex: ' [^{extraordinary-abilities-menu}](~@{character_id}|NPCPREFIXex_button)',
sp: ' [^{spell-like-abilities-menu}](~@{character_id}|NPCPREFIXsp_button)',
su: ' [^{supernatural-abilities-menu}](~@{character_id}|NPCPREFIXsu_button)',
},
events = {
attackEventsSLA: ['damage-macro-text', 'damage-type', 'abil-sr', 'save', 'abil-attack-type', 'name', 'range_numeric', 'toggle_attack_entry'],
commandMacroFields: ['name', 'used', 'used_max', 'showinmenu', 'ability_type', 'frequency', 'rule_category', 'toggle_attack_entry'],
};
function setClassName(id, callback, eventInfo) {
let done = _.once(function () {
if (typeof callback === 'function') {
callback();
}
}),
idStr = SWUtils.getRepeatingIDStr(id),
prefix = 'repeating_ability_' + idStr,
clbasisField = prefix + 'CL-basis';
getAttrs(
[prefix + 'CL-basis', prefix + 'class-name', 'character_name', 'race', 'class-0-name', 'class-1-name', 'class-2-name', 'class-3-name', 'class-4-name', 'class-5-name'],
function (v) {
let clBase = '',
setter = {},
match;
try {
if (v[clbasisField]) {
if (v[clbasisField] === '@{level}') {
clBase = v['race'];
} else if (v[clbasisField] === '@{npc-hd-num}') {
clBase = v['race'];
// added "Other" value="(0*(@{level}))" on Cl-Basis selector since "N/A" option uses value="0"
} else if (v[clbasisField] === '(0*(@{level}))') {
// if Class-name has been set by the user, prevent it from being overwritten when recalculating
if (v[prefix + 'class-name'] != 'undefined') {
clBase = v[prefix + 'class-name'];
setter[prefix + 'CL-basis-mod'] = 0;
} else {
clBase = '';
}
} else if (parseInt(v[clbasisField], 10) === 0) {
clBase = '';
} else {
match = v[prefix + 'CL-basis'].match(/\d+/);
if (match) {
clBase = v['class-' + match[0] + '-name'];
}
}
if (v[prefix + 'class-name'] !== clBase) {
setter[prefix + 'class-name'] = clBase;
}
}
} catch (err) {
TAS.error('PFAbility.setClassName', err);
} finally {
if (_.size(setter) > 0) {
SWUtils.setWrapper(setter, PFConst.silentParams, done);
} else {
done();
}
}
},
);
}
function setTypeTab(callback, silently, id, eventInfo) {
let prefix = 'repeating_ability_' + SWUtils.getRepeatingIDStr(id);
getAttrs(
[prefix + 'frequency', prefix + 'rule_category', prefix + 'CL-basis', prefix + 'ability_type', prefix + 'tabcat', prefix + 'tabcat2', 'abilities_tab', 'npc-abilities_tab'],
function (v) {
let setter = {},
params = PFConst.silentParams;
//TAS.debug("############ PFAbility setTypeTab",v);
setter[prefix + 'tabcat2'] = v[prefix + 'ability_type'] || '-1';
//TAS.notice("############","Ability setting ",setter);
SWUtils.setWrapper(setter, params);
},
);
}
function setRuleTab(callback, silently, id, eventInfo) {
let prefix = 'repeating_ability_' + SWUtils.getRepeatingIDStr(id);
getAttrs(
[prefix + 'frequency', prefix + 'rule_category', prefix + 'CL-basis', prefix + 'ability_type', prefix + 'tabcat', prefix + 'tabcat2', 'abilities_tab', 'npc-abilities_tab'],
function (v) {
let setter = {},
ruleForTab = '',
params = PFConst.silentParams;
switch (v[prefix + 'rule_category']) {
case 'racial-traits':
ruleForTab = 'traits';
break;
case 'monster-rule':
ruleForTab = 'other';
break;
case 'mythic-abilities':
ruleForTab = 'mythic';
break;
case 'mythic-feats':
ruleForTab = 'feats';
break;
default:
ruleForTab = v[prefix + 'rule_category'] || '';
break;
}
if (!ruleForTab) {
ruleForTab = '-1';
}
setter[prefix + 'tabcat'] = ruleForTab;
//if users changed the rule then change the tab we're checked on
if (eventInfo) {
if (ruleForTab !== '-1') {
if (v.abilities_tab !== ruleForTab && !/Ex|Sp|Su|99/i.test(v.abilities_tab)) {
setter.abilities_tab = ruleForTab;
}
if (v['npc-abilities_tab'] !== ruleForTab && !/Ex|Sp|Su|99/i.test(v['npc-abilities_tab'])) {
setter['npc-abilities_tab'] = ruleForTab;
}
}
if (v[prefix + 'rule_category'] === 'class-features' && (!v[prefix + 'CL-basis'] || v[prefix + 'CL-basis'] === '0')) {
setter[prefix + 'CL-basis'] = '@{class-0-level}';
params = {};
} else if (v[prefix + 'rule_category'] === 'racial-traits' && (!v[prefix + 'CL-basis'] || v[prefix + 'CL-basis'] === '0')) {
setter[prefix + 'CL-basis'] = '@{level}';
params = {};
}
}
if (v[prefix + 'rule_category'] === 'spell-like-abilities') {
v[prefix + 'tabcat2'] = 'Sp';
setter[prefix + 'ability_type'] = 'Sp';
}
if (!v[prefix + 'frequency']) {
setter[prefix + 'frequency'] = 'not-applicable';
}
if (_.size(setter)) {
//TAS.notice("############","Ability setting ",setter);
SWUtils.setWrapper(setter, params, setClassName);
}
},
);
}
export function setRuleTabs() {
getSectionIDs('repeating_ability', function (ids) {
_.each(ids, function (id) {
setRuleTab(null, null, id);
setTypeTab(null, null, id);
});
});
}
/** returns all rule_category and ability_type used
* @returns {'rules':[values of rule_category], 'types':[valuesof ability_type]} object of rules
*/
function getAbilityTypes(callback) {
let done = function (typeObj) {
//TAS.debug('Ability.getAbilityTypes returning with ',typeObj);
if (typeof callback === 'function') {
callback(typeObj);
}
};
getSectionIDs('repeating_ability', function (ids) {
let fields = [];
if (!ids || _.size(ids) === 0) {
done({rules: [], types: []});
return;
}
_.each(ids, function (id) {
let prefix = 'repeating_ability_' + id + '_';
fields.push(prefix + 'rule_category');
fields.push(prefix + 'showinmenu');
fields.push(prefix + 'ability_type');
});
getAttrs(fields, function (v) {
let basearray = [],
rulearray = [],
typearray = [];
basearray = _.chain(ids)
.map(function (id) {
let retObj = {},
prefix = 'repeating_ability_' + id + '_';
retObj.id = id;
retObj.showinmenu = parseInt(v[prefix + 'showinmenu'], 10) || 0;
retObj.rule_category = v[prefix + 'rule_category'] || '';
retObj.ability_type = (v[prefix + 'ability_type'] || '').toLowerCase();
//TAS.debug("row "+id+" is ",retObj);
return retObj;
})
.filter(function (o) {
return o.showinmenu;
})
.value();
if (basearray) {
rulearray = _.chain(basearray).groupBy('rule_category').keys().compact().value();
typearray = _.chain(basearray).groupBy('ability_type').keys().compact().value();
}
if (!rulearray) {
rulearray = [];
}
if (!typearray) {
typearray = [];
}
done({rules: rulearray, types: typearray});
});
});
}
function getNewAbilityAttrs(ability) {
let setter = {},
id = '',
prefix = '',
matches;
try {
id = generateRowID();
prefix = 'repeating_ability_' + id + '_';
setter[prefix + 'row_id'] = id;
setter[prefix + 'showinmenu'] = ability['showinmenu'] || 0;
setter[prefix + 'name'] = ability.name || '';
setter[prefix + 'used'] = ability['used'] || 0;
setter[prefix + 'used_max'] = ability['used_max'] || 0;
setter[prefix + 'max-calculation'] = ability['max-calculation'] || '';
setter[prefix + 'short-description'] = ability['short-description'] || '';
setter[prefix + 'description'] = ability['description'] || '';
setter[prefix + 'rule_category'] = ability['rule_category'] || '';
setter[prefix + 'CL-basis'] = ability['CL-basis'] || '0';
setter[prefix + 'class-name'] = ability['class-name'] || '';
if (ability.rule_category === 'spell-like-abilities') {
setter[prefix + 'ability_type'] = 'Sp';
if (ability['spell_level-misc']) {
setter[prefix + 'spell_level-misc'] = ability['spell_level-misc'];
setter[prefix + 'spell_level-misc-mod'] = ability['spell_level-misc-mod'];
setter[prefix + 'spell_level-basis'] = ability['spell_level-basis'];
}
if (ability['range_numeric']) {
setter[prefix + 'range'] = ability['range'];
setter[prefix + 'range_numeric'] = ability['range_numeric'];
setter[prefix + 'range_pick'] = ability['range_pick'];
}
if (ability['abil-sr']) {
setter[prefix + 'abil-sr'] = ability['abil-sr'];
}
if (ability['save']) {
setter[prefix + 'save'] = ability['save'];
}
} else {
matches = ability.name.match(/\b(Sp|Su|Ex)\b/i);
if (matches && matches[1]) {
setter[prefix + 'ability_type'] = matches[0][0].toUpperCase() + matches[0][1].toLowerCase();
} else {
setter[prefix + 'ability_type'] = '';
}
}
setter[prefix + 'macro-text'] = ability['macro-text'] || '';
} catch (err) {
TAS.error('PFAbility.getNewAbilityAttrs', err, ability);
} finally {
return setter;
}
}
export function copyToAbilities(callback, abilities) {
let done = _.once(function () {
//TAS.debug("leaving PFAbility.copyToAbilities");
if (typeof callback === 'function') {
callback();
}
}),
setter = {};
//TAS.debug("At PFAbility.copyToAbilities");
if (_.size(abilities)) {
_.each(abilities, function (ability) {
let xtra = getNewAbilityAttrs(ability);
//TAS.debug("PFAbility.copyToAbilities adding ",xtra);
_.extend(setter, xtra);
});
//TAS.debug("##########################","PFAbility.copyToAbilities setting",setter);
}
if (_.size(setter)) {
SWUtils.setWrapper(setter, PFConst.silentParams, done);
} else {
done();
}
}
/** resetTopCommandMacro sets all-abilities_buttons_macro (menu of ability menus) */
function getTopOfMenu(callback, isNPC) {
let done = function (str) {
//TAS.debug("leaving PFAbility.getTopOfMenu");
if (typeof callback === 'function') {
callback(str);
}
},
newMacro = '',
setter = {};
try {
newMacro = ' @{orig_ability_header_macro}';
getAbilityTypes(function (used) {
let addlMacros = '',
prefix = '';
try {
if (isNPC) {
prefix = 'NPC-';
}
if (used.types) {
_.each(used.types, function (type) {
if (otherCommandMacros[type]) {
addlMacros += otherCommandMacros[type].replace('NPCPREFIX', prefix);
} else if (type) {
TAS.warn('could not find top macro for ' + type);
}
});
}
if (addlMacros) {
newMacro += ' {{row03=^{ability-menus}}} {{row04=' + addlMacros + '}}';
}
//TAS.debug("PFAbility.getTopOfMenu: done building top macro it is :",newMacro);
} catch (innererr) {
TAS.error('PFAbility.getTopOfMenu innererr', innererr);
} finally {
done(newMacro);
}
});
} catch (err) {
TAS.error('PFAbility.getTopOfMenu', err);
done(newMacro);
}
}
export function resetCommandMacro(callback) {
let done = _.once(function () {
if (typeof callback === 'function') {
callback();
}
}),
doneOne = _.after(8, done);
getTopOfMenu(function (header) {
PFMenus.resetOneCommandMacro('ability', true, doneOne, header);
}, true);
getTopOfMenu(function (header) {
PFMenus.resetOneCommandMacro('ability', false, doneOne, header);
}, false);
PFMenus.resetOneCommandMacro('ex', true, doneOne);
PFMenus.resetOneCommandMacro('sp', true, doneOne);
PFMenus.resetOneCommandMacro('su', true, doneOne);
PFMenus.resetOneCommandMacro('ex', false, doneOne);
PFMenus.resetOneCommandMacro('sp', false, doneOne);
PFMenus.resetOneCommandMacro('su', false, doneOne);
}
export function importFromCompendium(callback, eventInfo) {
let done = _.once(function () {
resetCommandMacro();
//TAS.debug("leaving PFAbility.importFromCompendium");
if (typeof callback === 'function') {
callback();
}
}),
id = SWUtils.getRowId(eventInfo.sourceAttribute), //row doesn't really exist yet so get id from event
prefix = 'repeating_ability_' + id + '_';
//TAS.debug"at PFAbility.importFromCompendium for "+ prefix);
getAttrs(
[
'is_undead',
prefix + 'name',
prefix + 'compendium_category',
prefix + 'rule_category',
prefix + 'ability_type_compendium',
prefix + 'ability_type',
prefix + 'description',
prefix + 'range_from_compendium',
prefix + 'target_from_compendium',
prefix + 'area_from_compendium',
prefix + 'effect_from_compendium',
],
function (v) {
let compcat = '',
abilitytype = '',
ability_basis = '',
location = '',
setter = {},
newcat = '',
abilname = '',
silentSetter = {},
match,
note = '',
areaEffectText = '',
newRangeSettings;
try {
//TAS.debug("PFAbility.importFromCompendium got values: ",v);
if (v[prefix + 'ability_type_compendium']) {
abilitytype = v[prefix + 'ability_type_compendium'];
setter[prefix + 'ability_type'] = abilitytype;
silentSetter[prefix + 'ability_type_compendium'] = '';
}
compcat = v[prefix + 'compendium_category'];
silentSetter[prefix + 'compendium_category'] = '';
if (compcat) {
compcat = compcat.toLowerCase();
if (compcat === 'feats') {
newcat = 'feats';
} else if (compcat === 'monster rule') {
newcat = 'monster-rule';
} else if (compcat === 'spell') {
newcat = 'spell-like-abilities';
}
if (newcat === 'monster-rule') {
if (v[prefix + 'description']) {
match = v[prefix + 'description'].match(/Location\:\s*(.*)$/i);
//TAS.debug"matching "+match);
if (match && match[1]) {
location = SWUtils.trimBoth(match[1].toLowerCase());
match = location.match(/special qual|sq|special att|special abil|defens|spell/i);
if (match) {
switch (match[0]) {
case 'special qual':
case 'sq':
newcat = 'special-qualities';
break;
case 'special att':
newcat = 'special-attacks';
break;
case 'special abil':
newcat = 'special-abilities';
break;
case 'defens':
newcat = 'defensive-abilities';
break;
case 'spell':
newcat = 'spell-like-abilities';
break;
}
}
}
}
}
if (abilitytype === 'Sp' && !newcat) {
newcat = 'spell-like-abilities';
}
if (!abilitytype && newcat === 'spell-like-abilities') {
abilitytype = 'Sp';
setter[prefix + 'ability_type'] = 'Sp';
} else if (abilitytype === 'Sp' && !newcat) {
newcat = 'spell-like-abilities';
}
if (newcat) {
setter[prefix + 'rule_category'] = newcat;
} else {
note += compcat;
}
if (abilitytype === 'Sp') {
areaEffectText = v[prefix + 'target_from_compendium'] || v[prefix + 'area_from_compendium'] || v[prefix + 'effect_from_compendium'] || '';
setter[prefix + 'targets'] = areaEffectText;
if (v[prefix + 'range_from_compendium']) {
newRangeSettings = PFUtils.parseSpellRangeText(v[prefix + 'range_from_compendium'], areaEffectText);
setter[prefix + 'range_pick'] = newRangeSettings.dropdown;
setter[prefix + 'range'] = newRangeSettings.rangetext;
}
setter[prefix + 'ability-basis'] = '@{CHA-mod}';
} else if (v[prefix + 'name']) {
abilname = v[prefix + 'name'].toLowerCase();
abilname = abilname.match(/^[^(]+/);
if (PFDB.default.specialAttackDCAbilityBase[abilname]) {
ability_basis = PFDB.default.specialAttackDCAbilityBase[abilname];
} else {
ability_basis = 'CON';
}
if (ability_basis === 'CON' && parseInt(v.is_undead, 10)) {
ability_basis = 'CHA';
}
ability_basis = '@{' + ability_basis + '}';
setter[prefix + 'ability-basis'] = ability_basis;
}
}
} catch (err) {
TAS.error('PFAbility.importFromCompendium', err);
} finally {
if (_.size(silentSetter) > 0) {
SWUtils.setWrapper(silentSetter, PFConst.silentParams);
}
//TAS.debug"PFAbility.importFromCompendium, setting",setter);
if (_.size(setter) > 0) {
SWUtils.setWrapper(setter, {}, done);
} else {
done();
}
}
},
);
}
export function setAttackEntryVals(spellPrefix, weaponPrefix, v, setter, noName) {
let notes = '',
attackType = '';
setter = setter || {};
try {
attackType = PFUtils.findAbilityInString(v[spellPrefix + 'abil-attack-type']);
if (v[spellPrefix + 'name']) {
if (!noName) {
setter[weaponPrefix + 'name'] = v[spellPrefix + 'name'];
}
setter[weaponPrefix + 'source-ability-name'] = v[spellPrefix + 'name'];
}
if (attackType) {
setter[weaponPrefix + 'attack-type'] = v[spellPrefix + 'abil-attack-type'];
if (/CMB/i.test(attackType)) {
setter[weaponPrefix + 'vs'] = 'cmd';
} else if (/ranged/i.test(attackType)) {
setter[weaponPrefix + 'vs'] = 'touch';
setter[weaponPrefix + 'isranged'] = 1;
setter[weaponPrefix + 'range'] = v[spellPrefix + 'range_numeric'] || 0;
} else {
setter[weaponPrefix + 'vs'] = 'touch';
setter[weaponPrefix + 'range'] = 0;
}
}
if (v[spellPrefix + 'damage-macro-text']) {
setter[weaponPrefix + 'precision_dmg_macro'] = v[spellPrefix + 'damage-macro-text'];
if (attackType) {
setter[weaponPrefix + 'critical_dmg_macro'] = v[spellPrefix + 'damage-macro-text'];
} else {
setter[weaponPrefix + 'critical_dmg_macro'] = '';
}
}
if (v[spellPrefix + 'damage-type']) {
setter[weaponPrefix + 'precision_dmg_type'] = v[spellPrefix + 'damage-type'];
if (attackType) {
setter[weaponPrefix + 'critical_dmg_type'] = v[spellPrefix + 'damage-type'];
} else {
setter[weaponPrefix + 'critical_dmg_type'] = '';
}
}
if (v[spellPrefix + 'save']) {
if (notes) {
notes += ', ';
}
notes += '\n**Save:** ' + v[spellPrefix + 'save'] + ' **DC:** [[@{' + spellPrefix + 'savedc}]]';
}
if (v[spellPrefix + 'abil-sr']) {
if (notes) {
notes += ', ';
}
notes += '\n**Spell Resistance:** ' + v[spellPrefix + 'abil-sr'];
}
// include a link in the weapon notes to execute the spell from chat
let toggle_attack_entry = v[spellPrefix + 'toggle_attack_entry'];
if (toggle_attack_entry === 1) {
if (v[spellPrefix + 'name']) {
if (notes) {
notes += ', ';
}
notes += `\n[${v[spellPrefix + 'name']}](\~@\{character_name\}|${spellPrefix}roll)`;
}
}
if (toggle_attack_entry !== 1) {
if (v[spellPrefix + 'name']) {
if (notes) {
notes += '';
}
notes += '';
}
}
if (notes) {
setter[weaponPrefix + 'notes'] = notes;
}
} catch (err) {
TAS.error('PFAbility.setAttackEntryVals', err);
} finally {
return setter;
}
}
/**Triggered from a button in repeating spells
*@param {string} id the row id or null
*@param {string} weaponId if updating existing row
*/
export function createAttackEntryFromRow(id, callback, silently, eventInfo, weaponId) {
let done = _.once(function () {
//TAS.debug("leaving PFAbility.createAttackEntryFromRow");
if (typeof callback === 'function') {
callback();
}
}),
attribList = [],
itemId,
item_entry,
slaPrefix,
attributes = ['range_numeric', 'damage-macro-text', 'damage-type', 'abil-sr', 'savedc', 'save', 'abil-attack-type', 'name', 'toggle_attack_entry'];
if (id === 'DELETED') {
done();
return;
}
itemId = id || (eventInfo ? SWUtils.getRowId(eventInfo.sourceAttribute) : '');
item_entry = 'repeating_ability_' + SWUtils.getRepeatingIDStr(itemId);
slaPrefix = item_entry;
if (!itemId) {
TAS.warn('Cannot create usable attack entry from SLA since we cannot identify the row id');
done();
return;
}
attributes.forEach(function (attr) {
attribList.push(slaPrefix + attr);
});
//TAS.debug("PFAbility.createAttackEntryFromRow: attribList=" + attribList);
getAttrs(attribList, function (v) {
let newRowId = '',
setter = {},
prefix = 'repeating_weapon_',
idStr = '',
abilityexists = true,
deletedability = false,
params = {};
try {
if (_.size(v) === 0) {
abilityexists = false;
}
if (abilityexists) {
//TAS.debug("at PFAbility.createAttackEntryFromRow",v);
if (!PFUtils.findAbilityInString(v[slaPrefix + 'abil-attack-type']) && !v[slaPrefix + 'damage-macro-text']) {
TAS.warn('no attack to create for ability ' + v[slaPrefix + 'name'] + ', ' + itemId);
} else {
if (!weaponId) {
newRowId = generateRowID();
} else {
newRowId = weaponId;
}
idStr = newRowId + '_';
prefix += idStr;
setter = setAttackEntryVals(slaPrefix, prefix, v, setter, weaponId);
setter[prefix + 'source-ability'] = itemId;
setter[prefix + 'group'] = 'Special';
setter[prefix + 'link_type'] = PFAttacks.linkedAttackType.ability;
}
} else if (weaponId) {
setter['repeating_weapon_' + weaponId + '_source-ability'] = 'DELETED';
deletedability = true;
}
} catch (err) {
TAS.error('PFAbility.createAttackEntryFromRow', err);
} finally {
if (deletedability) {
SWUtils.setWrapper(setter, params, done);
} else if (_.size(setter) > 0) {
setter[slaPrefix + 'create-attack-entry'] = 0;
if (silently) {
params = PFConst.silentParams;
}
//TAS.debug("PFAbility.createAttackEntryFromRow setting:",setter);
SWUtils.setWrapper(setter, params, function () {
//can do these in parallel
//TAS.debug("PFAbility.createAttackEntryFromRow came back from setter ");
PFAttackOptions.resetOption(newRowId);
PFAttackGrid.resetCommandMacro();
done();
});
} else {
setter[slaPrefix + 'create-attack-entry'] = 0;
SWUtils.setWrapper(setter, PFConst.silentParams, done);
}
}
});
}
export function updateAssociatedAttack(id, callback, silently, eventInfo) {
let done = _.once(function () {
//TAS.debug("leaving PFAbility.updateAssociatedAttack");
if (typeof callback === 'function') {
callback();
}
}),
itemId = '',
item_entry = '',
attrib = '',
attributes = [];
itemId = id || (eventInfo ? SWUtils.getRowId(eventInfo.sourceAttribute) : '');
item_entry = 'repeating_ability_' + SWUtils.getRepeatingIDStr(itemId);
attrib = eventInfo ? SWUtils.getAttributeName(eventInfo.sourceAttribute) : '';
attributes = [];
//TAS.debug("at PF Spell like abilities updateAssociatedAttack: for row" + id );
if (attrib) {
attributes = [item_entry + attrib];
if (/range/i.test(attrib)) {
attributes = [item_entry + 'range_pick', item_entry + 'range', item_entry + 'range_numeric'];
} else {
attributes = [
item_entry + 'range_pick',
item_entry + 'range',
item_entry + 'range_numeric',
item_entry + 'damage-macro-text',
item_entry + 'damage-type',
item_entry + 'sr',
item_entry + 'savedc',
item_entry + 'save',
item_entry + 'abil-attack-type',
item_entry + 'name',
item_entry + 'toggle_attack_entry',
];
}
} else {
attributes = [
item_entry + 'range_pick',
item_entry + 'range',
item_entry + 'range_numeric',
item_entry + 'damage-macro-text',
item_entry + 'damage-type',
item_entry + 'sr',
item_entry + 'savedc',
item_entry + 'save',
item_entry + 'abil-attack-type',
item_entry + 'name',
item_entry + 'toggle_attack_entry',
];
}
getAttrs(attributes, function (spellVal) {
getSectionIDs('repeating_weapon', function (idarray) {
// get the repeating set
let spellsourcesFields = [];
spellsourcesFields = _.reduce(
idarray,
function (memo, currentID) {
memo.push('repeating_weapon_' + currentID + '_source-ability');
return memo;
},
[],
);
getAttrs(spellsourcesFields, function (v) {
let setter = {},
params = {},
idlist = [];
try {
_.each(idarray, function (currentID) {
let prefix = 'repeating_weapon_' + currentID + '_';
if (v[prefix + 'source-ability'] === itemId) {
idlist.push(currentID);
setter = setAttackEntryVals(item_entry, prefix, spellVal, setter);
}
});
} catch (err) {
TAS.error('PFAbility.updateAssociatedAttack', err);
} finally {
if (_.size(setter) > 0) {
if (silently) {
params = PFConst.silentParams;
}
SWUtils.setWrapper(setter, params, function () {
PFAttackOptions.resetSomeOptions(idlist);
});
} else {
done();
}
}
});
});
});
}
function updateCharLevel(id, callback, eventInfo) {
let done = _.once(function () {
//TAS.debug("leaving updateCharLevel");
if (typeof callback === 'function') {
callback();
}
}),
idStr = SWUtils.getRepeatingIDStr(id),
prefix = 'repeating_ability_' + idStr;
getAttrs([prefix + 'CL-misc-mod', prefix + 'CL-basis-mod', prefix + 'casterlevel', prefix + 'ability_type', 'buff_CasterLevel-total', 'CasterLevel-Penalty'], function (v) {
let clBase = 0,
cl = 0,
misc = 0,
pen = 0,
isSP = 0,
setter = {};
try {
isSP = parseInt(v[prefix + 'ability_type'], 10) || 0;
clBase = parseInt(v[prefix + 'CL-basis-mod'], 10) || 0;
misc = parseInt(v[prefix + 'CL-misc-mod'], 10) || 0;
pen = parseInt(v['CasterLevel-Penalty'], 10) || 0;
cl = clBase + misc + pen;
if (isSP) {
cl += parseInt(v['buff_CasterLevel-total'], 10) || 0;
}
if (cl !== parseInt(v[prefix + 'casterlevel'], 10)) {
setter[prefix + 'casterlevel'] = cl;
}
} catch (err) {
TAS.error('PFAbility.updateCharLevel', err);
} finally {
if (_.size(setter)) {
SWUtils.setWrapper(setter, PFConst.silentParams, done);
} else {
done();
}
}
});
}
function updateAbilityRange(id, callback, silently, eventInfo) {
let done = _.once(function () {
//TAS.debug("leaving updateAbilityRange");
if (typeof callback === 'function') {
callback();
}
}),
idStr = SWUtils.getRepeatingIDStr(id),
prefix = 'repeating_ability_' + idStr;
getAttrs([prefix + 'range_pick', prefix + 'range', prefix + 'range_numeric', prefix + 'casterlevel', prefix + 'ability_type'], function (v) {
let newRange = 0,
currRange = 0,
cl = 0,
setter = {},
isSP = 0,
currPosRange = 0;
try {
isSP = v[prefix + 'ability_type'] === 'Sp' ? 1 : 0;
currRange = parseInt(v[prefix + 'range_numeric'], 10) || 0;
if (isSP) {
cl = parseInt(v[prefix + 'casterlevel'], 10) || 0;
newRange = PFUtils.findSpellRange(v[prefix + 'range'], v[prefix + 'range_pick'], cl) || 0;
} else {
newRange = parseInt(SWUtils.trimBoth(v[prefix + 'range']), 10) || 0;
}
if (newRange !== currRange) {
//TAS.debug("updating range");
setter[prefix + 'range_numeric'] = newRange;
}
currPosRange = parseInt(v[prefix + 'hasposrange'], 10) || 0;
if (newRange > 0 && !currPosRange) {
setter[prefix + 'hasposrange'] = 1;
} else if (currPosRange) {
setter[prefix + 'hasposrange'] = 0;
}
} catch (err) {
TAS.error('updateAbilityRange', err);
} finally {
if (_.size(setter)) {
SWUtils.setWrapper(setter, {}, done);
} else {
done();
}
}
});
}
/** to use in calls to _.invoke or otherwise, sets switch variables to setter for given row
* @param {jsobj} setter to pass in first var of SWUtils.setWrapper
* @param {string} id the id of this row, or null if we are within the row context already
* @param {jsobj} v the values needed returned by getAttrs
*/
function resetOption(setter, id, v, eventInfo) {
let idStr = SWUtils.getRepeatingIDStr(id),
prefix = 'repeating_ability_' + idStr,
isSP = '',
posRange = '',
hasUses = '',
hasFrequency = '',
hasAttack = '',
atkstr = '',
attackStrForDisplay = '';
setter = setter || {};
try {
if (!v) {
return setter;
}
isSP = v[prefix + 'ability_type'] === 'Sp' ? '1' : '';
if (isSP !== v[prefix + 'is_sp']) {
setter[prefix + 'is_sp'] = isSP;
}
posRange = (parseInt(v[prefix + 'range_numeric'], 10) || 0) > 0 ? '1' : '';
if (posRange !== v[prefix + 'hasposrange']) {
setter[prefix + 'hasposrange'] = posRange;
}
if (v[prefix + 'frequency'] && v[prefix + 'frequency'] !== 'not-applicable') {
hasFrequency = '1';
switch (v[prefix + 'frequency']) {
case 'perday':
case 'permonth':
case 'hexfreq':
case 'other':
hasUses = '1';
break;
}
}
if (hasFrequency !== v[prefix + 'hasfrequency']) {
setter[prefix + 'hasfrequency'] = hasFrequency;
}
if (hasUses !== v[prefix + 'hasuses']) {
setter[prefix + 'hasuses'] = hasUses;
}
if (PFUtils.findAbilityInString(v[prefix + 'abil-attack-type'])) {
hasAttack = '1';
}
if (hasAttack !== v[prefix + 'hasattack']) {
setter[prefix + 'hasattack'] = hasAttack;
}
if (hasAttack) {
atkstr = v[prefix + 'abil-attack-type'].toLowerCase();
if (atkstr.indexOf('melee') >= 0) {
attackStrForDisplay = 'touch';
} else if (atkstr.indexOf('range') >= 0) {
attackStrForDisplay = 'ranged-touch-ray';
} else if (atkstr.indexOf('cmb') >= 0) {
attackStrForDisplay = 'combat-maneuver-bonus-abbrv';
}
}
if (attackStrForDisplay !== v[prefix + 'abil-attacktypestr']) {
setter[prefix + 'abil-attacktypestr'] = attackStrForDisplay;
}
} catch (err) {
TAS.error('PFAbility.recalcAbilities', err);
} finally {
return setter;
}
}
function resetOptionAsync(id, callback, eventInfo) {
let done = _.once(function () {
//TAS.debug("leaving PFAbility.resetOption");
if (typeof callback === 'function') {
callback();
}
}),
idStr = SWUtils.getRepeatingIDStr(id),
prefix = 'repeating_ability_' + idStr,
fields = [];
fields = _.map(allOptionRepeatingFields, function (attr) {
return prefix + attr;
});
getAttrs(fields, function (v) {
let setter = {};
try {
setter = resetOption(setter, id, v);
} catch (err) {
TAS.error('PFAbility.recalcAbilities', err);
} finally {
if (_.size(setter)) {
SWUtils.setWrapper(setter, PFConst.silentParams, done, eventInfo);
} else {
done();
}
}
});
}
function recalcAbilities(callback, silently, eventInfo, levelOnly) {
let done = _.once(function () {
//TAS.debug("leaving PFAbility.recalcAbilities");
if (typeof callback === 'function') {
callback();
}
});
getSectionIDs('repeating_ability', function (ids) {
let numids = _.size(ids),
doneOne,
calllevel;
if (numids === 0) {
done();
return;
}
doneOne = _.after(numids, done);
//refactor to do all rows at once
calllevel = function (id) {
PFUtilsAsync.setRepeatingDropdownValue(
'ability',
id,
'CL-basis',
'CL-basis-mod',
function () {
updateCharLevel(id, function () {
setClassName(id);
updateAbilityRange(id, function () {
doneOne();
});
});
},
true,
true,
);
};
_.each(ids, function (id) {
calllevel(id);
if (!levelOnly) {
resetOptionAsync(id);
}
});
});
}
export function migrate(callback) {
if (typeof callback === 'function') {
callback();
}
}
export let recalculate = TAS.callback(function callPFAbilityRecalculate(callback, silently, oldversion) {
let done = _.once(function () {
TAS.info('leaving PFAbility.recalculate');
if (typeof callback === 'function') {
callback();
}
}),
doneWithList = function () {
resetCommandMacro();
done();
},
callRecalcAbilities = function () {
//TAS.debug("PF1 calling recalcAbilities");
recalcAbilities(TAS.callback(doneWithList));
setRuleTabs();
};
try {
//TAS.debug("at PFAbility.recalculate");
migrate(TAS.callback(callRecalcAbilities));
} catch (err) {
TAS.error('PFAbility.recalculate, ', err);
done();
}
});
//sync repeating_ability with settings>attacks>link spells
function updateIncludeLink() {
getSectionIDs('repeating_ability', function (ids) {
_.each(ids, function (id) {
getAttrs(['include_link_abilities'], function (values) {
let include_link_abilities = parseInt([values.include_link_abilities]) || 0;
setAttrs({
['repeating_ability_' + id + '_toggle_attack_entry']: include_link_abilities,
});
});
});
});
}
//trigger updateIncludeLink when updating the sheet version
export function updateIncludeLinkVersionCheck() {
setAttrs({
include_link_abilities: 1,
include_link_spells: 1,
});
}
function registerEventHandlers() {
let eventToWatch = '',
macroEvent = 'remove:repeating_ability ',
singleEvent = 'change:repeating_ability:';
on(
'remove:repeating_ability',
TAS.callback(function eventRemoveAbility(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
PFAttacks.removeLinkedAttack(null, PFAttacks.linkedAttackType.ability, SWUtils.getRowId(eventInfo.sourceAttribute));
}),
);
macroEvent = _.reduce(
events.commandMacroFields,
function (m, a) {
m += singleEvent + a + ' ';
return m;
},
macroEvent,
);
on(
macroEvent,
TAS.callback(function eventRepeatingAbilityCommandMacroUpdate(eventInfo) {
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api' || /used_max/i.test(eventInfo.sourceAttribute)) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
PFFeatures.resetTopCommandMacro(null, eventInfo);
resetCommandMacro();
}
}),
);
on(
'change:repeating_ability:CL-basis',
TAS.callback(function eventAbilityClassDropdown(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
SWUtils.evaluateAndSetNumber('repeating_ability_CL-basis', 'repeating_ability_CL-basis-mod');
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api') {
setClassName(null, null, eventInfo);
}
}),
);
eventToWatch = _.reduce(
optionRepeatingHelperFields,
function (m, a) {
m += 'change:repeating_ability:' + a + ' ';
return m;
},
'',
);
on(
eventToWatch,
TAS.callback(function eventChangeAbilityTypeFrequencyOrRange(eventInfo) {
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api' || /range/i.test(eventInfo.sourceAttribute)) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
resetOptionAsync();
}
}),
);
on(
'change:repeating_ability:CL-misc change:repeating_ability:spell_level-misc',
TAS.callback(function eventSLAEquationMacro(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
SWUtils.evaluateAndSetNumber(eventInfo.sourceAttribute, eventInfo.sourceAttribute + '-mod');
}),
);
on(
'change:buff_CasterLevel-total change:CasterLevel-Penalty',
TAS.callback(function eventAbilityLevelChange(eventInfo) {
if (eventInfo.sourceType === 'sheetworker' || eventInfo.sourceType === 'api') {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
recalcAbilities(null, null, eventInfo, true);
}
}),
);
on(
'change:repeating_ability:CL-basis-mod change:repeating_ability:CL-misc-mod',
TAS.callback(function eventAbilityLevelChange(eventInfo) {
if (eventInfo.sourceType === 'sheetworker' || eventInfo.sourceType === 'api') {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
updateCharLevel(null, null, eventInfo);
}
}),
);
on(
'change:repeating_ability:compendium_category',
TAS.callback(function eventAbilityCompendium(eventInfo) {
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api') {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
importFromCompendium(null, eventInfo);
}
}),
);
on(
'change:repeating_ability:create-attack-entry',
TAS.callback(function eventcreateAttackEntryFromSLA(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api') {
createAttackEntryFromRow(null, null, false, eventInfo);
}
}),
);
on(
'change:repeating_ability:CL-misc-mod change:repeating_ability:CL-basis-mod change:repeating_ability:range_pick change:repeating_ability:range',
TAS.callback(function eventClassRangeMod(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
//cl-misc-mod, cl-basis-mod is sheetworker, range_pick and range must be player
if (
(/range/i.test(eventInfo.sourceAttribute) && (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api')) ||
(/CL/i.test(eventInfo.sourceAttribute) && eventInfo.sourceType === 'sheetworker') ||
eventInfo.sourceType === 'api'
) {
updateAbilityRange(null, null, false, eventInfo);
}
}),
);
eventToWatch = _.reduce(
events.attackEventsSLA,
function (memo, attr) {
memo += 'change:repeating_ability:' + attr + ' ';
return memo;
},
'',
);
on(
eventToWatch,
TAS.callback(function eventupdateAssociatedSLAttackAttack(eventInfo) {
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api' || /range_numeric/i.test(eventInfo.sourceAttribute)) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
updateAssociatedAttack(null, null, null, eventInfo);
}
}),
);
on(
'change:repeating_ability:toggle_attack_entry',
TAS.callback(function eventupdateAssociatedSLAttackAttack(eventInfo) {
if (eventInfo.sourceType === 'sheetworker' || eventInfo.sourceType === 'api') {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
updateAssociatedAttack(null, null, null, eventInfo);
}
}),
);
on(
'change:repeating_ability:rule_category',
TAS.callback(function eventUpdateAbilityRule(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
setRuleTab(null, null, null, eventInfo);
}),
);
on(
'change:repeating_ability:ability_type',
TAS.callback(function eventUpdateAbilityType(eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
setTypeTab(null, null, null, eventInfo);
}),
);
// toggles the chat menu Show option for all repeating abilities
on('change:showinmenu_all_abilities', function () {
getSectionIDs('repeating_ability', function (idArrayAbility) {
getSectionIDs('repeating_mythic-ability', function (idArrayMythicAbility) {
getSectionIDs('repeating_mythic-feat', function (idArrayMythicFeat) {
const fieldNamesAbility = idArrayAbility.map((id) => `repeating_ability_${id}_showinmenu`),
fieldNamesMythicAbility = idArrayMythicAbility.map((id) => `repeating_mythic-ability_${id}_showinmenu`),
fieldNamesMythicFeat = idArrayMythicFeat.map((id) => `repeating_mythic-feat_${id}_showinmenu`);
getAttrs(['showinmenu_all_abilities'], function (values) {
const toggle = +values['showinmenu_all_abilities'] || 0,
settings = {};
fieldNamesAbility.forEach((field) => {
// loop through the showinmenu array
settings[field] = toggle; // assign the toggle value to each row's attribute
});
fieldNamesMythicAbility.forEach((field) => {
settings[field] = toggle;
});
fieldNamesMythicFeat.forEach((field) => {
settings[field] = toggle;
});
setAttrs(settings);
PFAbility.recalculate();
PFFeatures.recalculate();
});
});
});
});
});
//sync repeating_abilities with settings>attacks>link abilities
on('change:include_link_abilities', function (eventInfo) {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event' + eventInfo.sourceType);
if (eventInfo.sourceType === 'sheetworker' || eventInfo.sourceType === 'player') {
updateIncludeLink();
}
});
// dynamic translation of datalist for ability_type2
on('change:repeating_ability:ability_type2', (eventInfo) => {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event' + eventInfo.sourceType);
const id = eventInfo.sourceAttribute.split('_')[2];
const updateAttrs = {};
const featName = eventInfo.newValue.replace(/\s/g, '').toLowerCase();
let translation = getTranslationByKey(featName);
if (translation) {
console.info({featName, translation});
updateAttrs[`repeating_ability_${id}_ability_type2`] = translation;
const attr = `repeating_ability_${id}_ability_type2`;
const i18n = `${featName}`;
if (getTranslationByKey(i18n)) {
updateAttrs[attr] = getTranslationByKey(i18n);
}
TAS.debug(updateAttrs);
setAttrs(updateAttrs, {silent: true});
}
});
on('change:repeating_ability:ability_type2 change:repeating_ability:rule_category', (eventInfo) => {
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event' + eventInfo.sourceType);
PFMacros.checkAbilityType2Row(eventInfo);
});
}
registerEventHandlers();