mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-09-11 01:22:32 +00:00
435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
/* global on:false, generateRowID:false */
|
|
|
|
import { ABILITIES } from './constants';
|
|
import { getSetItems, getIntValue, isUndefinedOrEmpty, addArithmeticOperator, getAbilityMod, numberWithCommas, exists, updateHD } from './utilities';
|
|
|
|
export class Npc {
|
|
updateType() {
|
|
getSetItems('npc.updateType', {
|
|
collectionArray: ['type'],
|
|
callback: (v, finalSetAttrs) => {
|
|
if (v.type) {
|
|
finalSetAttrs.type = v.type.toLowerCase();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateSize() {
|
|
getSetItems('npc.updateSize', {
|
|
collectionArray: ['size'],
|
|
callback: (v, finalSetAttrs) => {
|
|
const creatureSize = v.size || 'Large';
|
|
const sizeToHdSize = {
|
|
Tiny: 4,
|
|
Small: 6,
|
|
Medium: 8,
|
|
Large: 10,
|
|
Huge: 12,
|
|
Gargantuan: 20,
|
|
};
|
|
finalSetAttrs.hit_die = `d${sizeToHdSize[creatureSize]}`;
|
|
},
|
|
});
|
|
}
|
|
updateAC() {
|
|
getSetItems('npc.updateAC', {
|
|
collectionArray: ['ac_srd', 'ac', 'ac_note', 'dexterity_mod'],
|
|
callback: (v, finalSetAttrs) => {
|
|
if (exists(v.ac_srd)) {
|
|
const match = v.ac_srd.match(/(\d+)\s?(.*)/);
|
|
if (match && match[1]) {
|
|
finalSetAttrs.AC = match[1];
|
|
}
|
|
if (match && match[2]) {
|
|
finalSetAttrs.ac_note = match[2].replace(/\(|\)/g, '');
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
switchTo() {
|
|
getSetItems('npc.switchTo', {
|
|
collectionArray: ['is_npc', 'size'],
|
|
callback: (v, finalSetAttrs) => {
|
|
const isNPC = getIntValue(v.is_npc) === 1;
|
|
|
|
if (isNPC && isUndefinedOrEmpty(v.size)) {
|
|
finalSetAttrs.size = 'Large';
|
|
}
|
|
|
|
if (isNPC) {
|
|
finalSetAttrs.hit_dice_output_option = '/w GM';
|
|
} else {
|
|
finalSetAttrs.hit_dice_output_option = '';
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateChallenge() {
|
|
const xpPerChallenge = {
|
|
0: 0,
|
|
'1/8': 25,
|
|
'1/4': 50,
|
|
'1/2': 100,
|
|
1: 200,
|
|
2: 450,
|
|
3: 700,
|
|
4: 1100,
|
|
5: 1800,
|
|
6: 2300,
|
|
7: 2900,
|
|
8: 3900,
|
|
9: 5000,
|
|
10: 5900,
|
|
11: 7200,
|
|
12: 8400,
|
|
13: 10000,
|
|
14: 11500,
|
|
15: 13000,
|
|
16: 15000,
|
|
17: 18000,
|
|
18: 20000,
|
|
19: 22000,
|
|
20: 25000,
|
|
21: 33000,
|
|
22: 41000,
|
|
23: 50000,
|
|
24: 62000,
|
|
25: 75000,
|
|
26: 90000,
|
|
27: 105000,
|
|
28: 120000,
|
|
29: 135000,
|
|
30: 155000,
|
|
};
|
|
|
|
getSetItems('npc.updateChallenge', {
|
|
collectionArray: ['challenge', 'xp'],
|
|
callback: (v, finalSetAttrs) => {
|
|
const challenge = v.challenge;
|
|
|
|
finalSetAttrs.xp = xpPerChallenge[challenge];
|
|
finalSetAttrs.xp_readable = numberWithCommas(finalSetAttrs.xp);
|
|
|
|
finalSetAttrs.level = challenge;
|
|
if (finalSetAttrs.level < 1) {
|
|
finalSetAttrs.level = 1;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateHPFromSRD() {
|
|
getSetItems('npc.updateHPFromSRD', {
|
|
collectionArray: ['hp_srd', 'constitution', 'constitution_mod', 'constitution_bonus', 'global_ability_bonus'],
|
|
callback: (v, finalSetAttrs) => {
|
|
if (exists(v.hp_srd)) {
|
|
const match = v.hp_srd.match(/\((\d+)d(\d+)(?:\s?(\+|\-)\s?(\d+))?\)/i);
|
|
if (!match || !match[1] || !match[2]) {
|
|
console.warn('Character doesn\'t have valid HP/HD format');
|
|
} else {
|
|
const hdNum = getIntValue(match[1]);
|
|
|
|
let conMod = getIntValue(v.constitution_mod);
|
|
if (!conMod) {
|
|
const conScore = getIntValue(v.constitution);
|
|
const conBonus = getIntValue(v.constitution_bonus);
|
|
const globalAbilityBonus = getIntValue(v.global_ability_bonus);
|
|
|
|
conMod = getAbilityMod((conScore + conBonus + globalAbilityBonus));
|
|
}
|
|
|
|
finalSetAttrs.hit_dice = hdNum;
|
|
finalSetAttrs.hit_die = `d${getIntValue(match[2])}`;
|
|
|
|
const hpExpectedBonus = hdNum * conMod;
|
|
const hpBonusSign = match[3];
|
|
const hpBonus = getIntValue(match[4]);
|
|
if (hpBonus !== hpExpectedBonus) {
|
|
if (hpBonusSign === '-') {
|
|
finalSetAttrs.hp_extra = hpBonus + hpExpectedBonus;
|
|
} else {
|
|
finalSetAttrs.hp_extra = hpBonus - hpExpectedBonus;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateHP() {
|
|
getSetItems('npc.updateHP', {
|
|
collectionArray: ['HP', 'HP_max', 'hp_formula', 'hit_dice', 'hit_die', 'hp_extra', 'constitution_mod'],
|
|
callback: (v, finalSetAttrs) => {
|
|
const hdNum = getIntValue(v.hit_dice);
|
|
const hdSize = getIntValue(v.hit_die.replace('d', ''));
|
|
const hdAverage = (hdSize / 2) + 0.5;
|
|
let hpFormula = `${hdNum}d${hdSize}`;
|
|
const conMod = getIntValue(v.constitution_mod);
|
|
let totalHP = Math.floor(hdNum * hdAverage);
|
|
let amount;
|
|
|
|
if (conMod !== 0) {
|
|
const bonusHP = hdNum * conMod;
|
|
totalHP += bonusHP;
|
|
hpFormula += addArithmeticOperator(hpFormula, bonusHP);
|
|
}
|
|
|
|
if (exists(v.hp_extra)) {
|
|
const regex = (/(?:(\+|\-)\s?)?(\d+)(?:d(\d+))?/gi);
|
|
let splitFormula;
|
|
|
|
while ((splitFormula = regex.exec(v.hp_extra)) !== null) {
|
|
if (!splitFormula || !splitFormula[2]) {
|
|
console.warn('Character doesn\'t have valid hp formula');
|
|
} else {
|
|
amount = 0;
|
|
|
|
if (!splitFormula[3]) {
|
|
amount = getIntValue(splitFormula[2]);
|
|
} else {
|
|
const extraHdNum = getIntValue(splitFormula[2]);
|
|
const extraHdSize = getIntValue(splitFormula[3]);
|
|
const extraHdAverage = (extraHdSize / 2) + 0.5;
|
|
amount = Math.floor(extraHdNum * extraHdAverage);
|
|
}
|
|
|
|
if (!splitFormula[1] || splitFormula[1] === '+') {
|
|
totalHP += amount;
|
|
hpFormula += ` + ${amount}`;
|
|
} else if (splitFormula[1] === '-') {
|
|
totalHP -= amount;
|
|
hpFormula += ` - ${amount}`;
|
|
}
|
|
}
|
|
v.hp_extra.toString().replace(splitFormula[0], '');
|
|
}
|
|
}
|
|
|
|
if (totalHP) {
|
|
finalSetAttrs.HP = totalHP;
|
|
finalSetAttrs.HP_max = totalHP;
|
|
finalSetAttrs.hp_formula = hpFormula;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateHD() {
|
|
const hd = {
|
|
d20: 0,
|
|
d12: 0,
|
|
d10: 0,
|
|
d8: 0,
|
|
d6: 0,
|
|
d4: 0,
|
|
d2: 0,
|
|
d0: 0,
|
|
};
|
|
const collectionArray = ['hit_dice', 'hit_die'];
|
|
for (const key in hd) {
|
|
if (hd.hasOwnProperty(key)) {
|
|
collectionArray.push(`hd_${key}`);
|
|
collectionArray.push(`hd_${key}_max`);
|
|
collectionArray.push(`hd_${key}_query`);
|
|
collectionArray.push(`hd_${key}_toggle`);
|
|
}
|
|
}
|
|
getSetItems('npc.updateHD', {
|
|
collectionArray,
|
|
callback: (v, finalSetAttrs) => {
|
|
const hdNum = getIntValue(v.hit_dice);
|
|
const hdSize = v.hit_die;
|
|
|
|
if (hdNum && hdSize) {
|
|
hd[hdSize] = hdNum;
|
|
|
|
updateHD(v, finalSetAttrs, hd);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
parseSRDContentSection(content, finalSetAttrs, title, name) {
|
|
const re = /@(.*)@:\s([^@]+)/gi;
|
|
let match;
|
|
let section;
|
|
|
|
if (content.indexOf(title) !== -1) {
|
|
const contentSplit = content.split(`${title}\n`);
|
|
section = contentSplit[1];
|
|
content = contentSplit[0];
|
|
}
|
|
if (exists(section)) {
|
|
if (name === 'legendaryaction') {
|
|
const legendaryActionsMatch = section.match(/Can take (\d+) Legendary Actions/i);
|
|
if (legendaryActionsMatch && legendaryActionsMatch[1]) {
|
|
finalSetAttrs.legendary_action_amount = legendaryActionsMatch[1];
|
|
}
|
|
}
|
|
|
|
while ((match = re.exec(section.replace(/\*\*/g, '@'))) !== null) {
|
|
if (match && match[1] && match[2]) {
|
|
const repeatingString = `repeating_${name}_${generateRowID()}_`;
|
|
finalSetAttrs[`${repeatingString}name`] = match[1];
|
|
const text = match[2].trim();
|
|
if (name === 'trait') {
|
|
finalSetAttrs[`${repeatingString}display_text`] = text;
|
|
}
|
|
finalSetAttrs[`${repeatingString}freetext`] = text;
|
|
} else {
|
|
console.warn(`Character doesn\'t have a valid ${name} format`);
|
|
}
|
|
}
|
|
}
|
|
return content;
|
|
}
|
|
setDefaultAbility(v, finalSetAttrs) {
|
|
const abilityScores = [getIntValue(v.intelligence), getIntValue(v.wisdom), getIntValue(v.charisma)];
|
|
const highestAbilityScore = Math.max.apply(Math, abilityScores);
|
|
let highestAbilityName = 'intelligence';
|
|
|
|
if (highestAbilityScore === abilityScores[1]) {
|
|
highestAbilityName = 'wisdom';
|
|
} else if (highestAbilityScore === abilityScores[2]) {
|
|
highestAbilityName = 'charisma';
|
|
}
|
|
|
|
finalSetAttrs.default_ability = highestAbilityName;
|
|
}
|
|
updateContent() {
|
|
const collectionArray = ['content_srd'];
|
|
for (const ability of ABILITIES) {
|
|
collectionArray.push(ability);
|
|
}
|
|
|
|
getSetItems('npc.updateContent', {
|
|
collectionArray,
|
|
callback: (v, finalSetAttrs) => {
|
|
let content = v.content_srd;
|
|
let regionalEffects;
|
|
let lairActions;
|
|
let repeatingString;
|
|
|
|
this.setDefaultAbility(v, finalSetAttrs);
|
|
|
|
if (exists(content)) {
|
|
if (content.indexOf('Regional Effects') !== -1) {
|
|
const regionalEffectsSplit = content.split(/Regional Effects\n/);
|
|
regionalEffects = regionalEffectsSplit[1];
|
|
content = regionalEffectsSplit[0];
|
|
}
|
|
if (exists(regionalEffects)) {
|
|
const regionalEffectsList = regionalEffects.split(/\*\*/);
|
|
regionalEffectsList.slice(1, -1).forEach((regionalEffect) => {
|
|
repeatingString = `repeating_regionaleffect_${generateRowID()}_`;
|
|
finalSetAttrs[`${repeatingString}freetext`] = regionalEffect.trim();
|
|
});
|
|
}
|
|
if (content.indexOf('Lair Actions') !== -1) {
|
|
const lairActionsSplit = content.split(/Lair Actions\n/);
|
|
lairActions = lairActionsSplit[1];
|
|
content = lairActionsSplit[0];
|
|
}
|
|
if (exists(lairActions)) {
|
|
lairActions.split(/\*\*/).slice(1).forEach((lairAction) => {
|
|
repeatingString = `repeating_lairaction_${generateRowID()}_`;
|
|
finalSetAttrs[`${repeatingString}freetext`] = lairAction.trim();
|
|
});
|
|
}
|
|
|
|
content = this.parseSRDContentSection(content, finalSetAttrs, 'Legendary Actions', 'legendaryaction');
|
|
content = this.parseSRDContentSection(content, finalSetAttrs, 'Reactions', 'reaction');
|
|
content = this.parseSRDContentSection(content, finalSetAttrs, 'Actions', 'action');
|
|
this.parseSRDContentSection(content, finalSetAttrs, 'Traits', 'trait');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateSenses() {
|
|
getSetItems('npc.updateSenses', {
|
|
collectionArray: ['senses'],
|
|
callback: (v, finalSetAttrs) => {
|
|
if (v.senses) {
|
|
finalSetAttrs.senses_exist = 1;
|
|
finalSetAttrs.senses = v.senses.toLowerCase();
|
|
} else {
|
|
finalSetAttrs.senses_exist = 0;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateLanguages() {
|
|
getSetItems('npc.updateLanguages', {
|
|
collectionArray: ['languages', 'languages_exist', 'languages_chat_var'],
|
|
callback: (v, finalSetAttrs) => {
|
|
finalSetAttrs.languages_exist = 0;
|
|
finalSetAttrs.languages_chat_var = '';
|
|
|
|
if (v.languages) {
|
|
finalSetAttrs.languages_exist = 1;
|
|
finalSetAttrs.languages_chat_var = '{{Languages=@{languages}}}';
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateSpeed() {
|
|
getSetItems('npc.updateSpeed', {
|
|
collectionArray: ['npc_speed'],
|
|
callback: (v, finalSetAttrs) => {
|
|
finalSetAttrs.npc_speed = v.npc_speed.toLowerCase();
|
|
const match = finalSetAttrs.npc_speed.match(/^\s*(\d+)\s*ft/);
|
|
if (match && match[1]) {
|
|
finalSetAttrs.speed = match[1];
|
|
}
|
|
},
|
|
});
|
|
}
|
|
updateACNote() {
|
|
getSetItems('npc.updateACNote', {
|
|
collectionArray: ['ac_note'],
|
|
callback: (v, finalSetAttrs) => {
|
|
finalSetAttrs.ac_note = v.ac_note.toLowerCase();
|
|
},
|
|
});
|
|
}
|
|
setup() {
|
|
on('change:type', () => {
|
|
this.updateType();
|
|
});
|
|
on('change:size', () => {
|
|
this.updateSize();
|
|
});
|
|
on('change:ac_srd', () => {
|
|
this.updateAC();
|
|
});
|
|
on('change:is_npc', () => {
|
|
this.switchTo();
|
|
});
|
|
on('change:challenge', () => {
|
|
this.updateChallenge();
|
|
});
|
|
on('change:hp_srd', () => {
|
|
this.updateHPFromSRD();
|
|
});
|
|
on('change:hit_dice change:hit_die change:hp_extra change:constitution_mod', () => {
|
|
this.updateHP();
|
|
});
|
|
on('change:hit_dice change:hit_die', () => {
|
|
this.updateHD();
|
|
});
|
|
on('change:content_srd', () => {
|
|
this.updateContent();
|
|
});
|
|
on('change:senses', () => {
|
|
this.updateSenses();
|
|
});
|
|
on('change:languages', () => {
|
|
this.updateLanguages();
|
|
});
|
|
on('change:npc_speed', () => {
|
|
this.updateSpeed();
|
|
});
|
|
on('change:ac_note', () => {
|
|
this.updateACNote();
|
|
});
|
|
}
|
|
}
|