Files
MaterialDeck/src/systems/pf2e.js

513 lines
20 KiB
JavaScript

import {compatibleCore} from "../misc.js";
import {otherControls} from "../../MaterialDeck.js";
const limitedSheets = ['loot', 'vehicle'];
const proficiencyColors =
{
untrained: "#424242",
trained: "#171F69",
expert: "#3C005E",
master: "#664400",
legendary: "#5E0000"
};
export class pf2e{
constructor(){
}
tokenSpellData = new Map();
getHP(token) {
const hp = token.actor.attributes?.hp;
return {
value: (hp?.value == null) ? 0 : hp.value,
max: (hp?.max == null) ? 0 : hp.max
}
}
getTempHP(token) {
const hp = token.actor.attributes?.hp;
return {
value: (hp?.temp == null) ? 0 : hp.temp,
max: (hp?.tempmax == null) ? 0 : hp.tempmax
}
}
getAC(token) {
const ac = token.actor.attributes?.ac;
return (ac?.value == null) ? 10 : ac?.value;
}
getShieldHP(token) {
const shieldhp = token.actor.attributes.shield
return (shieldhp?.value == null) ? 0 : shieldhp?.value;
}
getSpeed(token) {
if (this.isLimitedSheet(token.actor) || token.actor.type == 'hazard') {
if (token.actor.type == 'vehicle') {
return token.actor.data.data.details.speed;
} else return '';
}
let speed = `${token.actor.attributes.speed?.total}'`;
const otherSpeeds = token.actor.attributes.speed?.otherSpeeds;
if (otherSpeeds.length > 0)
for (let os of otherSpeeds)
speed += `\n${os.type} ${os.total}'`;
return speed;
}
getInitiative(token) {
if (this.isLimitedSheet(token.actor) || token.actor.type == 'familiar') return '';
if (token.actor.type == 'hazard') {
let initiative = token.actor.attributes?.stealth?.value;
return `Init: Stealth (${initiative})`;
}
let initiative = token.actor.attributes.initiative;
let initiativeModifier = initiative?.totalModifier;
let initiativeLabel = initiative?.label.replace('iative',''); //Initiative is too long for the button
if (initiativeModifier > 0) {
initiativeModifier = `+${initiativeModifier}`;
} else {
initiativeModifier = this.getPerception(token); //NPCs won't have a valid Initiative value, so default to use Perception
}
return `${initiativeLabel} (${initiativeModifier})`;
}
toggleInitiative(token) {
return;
}
getPassivePerception(token) {
return;
}
getPassiveInvestigation(token) {
return;
}
getPerception(token) {
if (this.isLimitedSheet(token.actor) || token.actor.type == 'hazard') return '';
let perception = token.actor.attributes.perception?.totalModifier;
return (perception >= 0) ? `+${perception}` : perception;
}
getAbility(token, ability) {
if (this.isLimitedSheet(token.actor) || token.actor.type == 'familiar') return '';
if (ability == undefined) ability = 'str';
return token.actor.abilities?.[ability]?.value;
}
getAbilityModifier(token, ability) {
if (this.isLimitedSheet(token.actor) || token.actor.type == 'hazard' || token.actor.type == 'familiar') return '';
if (ability == undefined) ability = 'str';
let val = token.actor.abilities?.[ability]?.mod;
return (val >= 0) ? `+${val}` : val;
}
getAbilitySave(token, ability) {
ability = this.fixSave(ability);
const save = this.findSave(token, ability);
if (save == undefined) return '';
let val = save?.value;
return (val >= 0) ? `+${val}` : val;
}
findSave(token, ability) {
if (this.isLimitedSheet(token.actor)) return;
return token.actor.data.data.saves?.[ability];
}
fixSave(ability) {
if (ability == undefined) return 'fortitude';
else if (ability == 'fort') return 'fortitude';
else if (ability == 'ref') return 'reflex';
else if (ability == 'will') return 'will';
}
getSkill(token, skill) {
const tokenSkill = this.findSkill(token, skill);
if (tokenSkill == undefined) return '';
if (skill.startsWith('lor')) {
return `${tokenSkill.name}: +${tokenSkill.totalModifier}`;
}
const val = tokenSkill.totalModifier;
return (val >= 0) ? `+${val}` : val;
}
findSkill(token, skill) {
if (this.isLimitedSheet(token.actor)) return;
if (skill == undefined) skill = 'acr';
if (skill.startsWith('lor')) {
const index = parseInt(skill.split('_')[1])-1;
const loreSkills = this.getLoreSkills(token);
if (loreSkills.length > index) {
return loreSkills[index];
} else {
return;
}
}
return token.actor.data.data.skills?.[skill];
}
getLoreSkills(token) {
if (this.isLimitedSheet(token.actor)) return [];
const skills = token.actor.data.data.skills;
return Object.keys(skills).map(key => skills[key]).filter(s => s.lore == true);
}
getProficiency(token) {
return;
}
getCondition(token,condition) {
if (condition == undefined || condition == 'removeAll') return undefined;
const Condition = this.getConditionName(condition);
const effects = token.actor.items.filter(i => i.type == 'condition');
return effects.find(e => e.name === Condition);
}
getConditionIcon(condition) {
if (condition == undefined) condition = 'removeAll';
if (condition == 'removeAll') return window.CONFIG.controlIcons.effects;
else return `${CONFIG.PF2E.statusEffects.effectsIconFolder}${condition}.webp`;
}
getConditionActive(token,condition) {
return this.getCondition(token,condition) != undefined;
}
getConditionValue(token,condition) {
const effect = this.getCondition(token, condition);
if (effect != undefined && effect?.value != null) return effect;
}
async modifyConditionValue(token,condition,delta) {
if (condition == undefined) condition = 'removeAll';
if (condition == 'removeAll'){
for( let effect of token.actor.items.filter(i => i.type == 'condition'))
await effect.delete();
} else {
const effect = this.getConditionValue(token,condition);
if (effect == undefined) {
if (delta > 0) {
await game.pf2e.ConditionManager.addConditionToToken(condition, token);
}
} else {
try {
await game.pf2e.ConditionManager.updateConditionValue(effect.id, token, effect.value+delta);
} catch (error) {
//Do nothing. updateConditionValue will have an error about 'documentData is not iterable' when called from an NPC token.
}
}
}
return true;
}
getConditionName(condition) {
if ("flatFooted" == condition) {
return 'Flat-Footed'; //An inconsistency has been introduced on the PF2E system. The icon is still using 'flatFooted' as the name, but the condition in the manager has been renamed to 'Flat-Footed'
} else return condition.charAt(0).toUpperCase() + condition.slice(1);
}
async toggleCondition(token,condition) {
if (condition == undefined) condition = 'removeAll';
if (condition == 'removeAll'){
for( let existing of token.actor.items.filter(i => i.type == 'condition'))
await game.pf2e.ConditionManager.removeConditionFromToken(existing.data._id, token);
}
else {
const effect = this.getCondition(token,condition);
if (effect == undefined) {
await game.pf2e.ConditionManager.addConditionToToken(condition, token);
}
else {
await game.pf2e.ConditionManager.removeConditionFromToken(effect.data._id, token);
}
}
return true;
}
/**
* Roll
*/
roll(token,roll,options,ability,skill,save) {
if (this.isLimitedSheet(token.actor)) return;
options.skipDialog = true;
if (roll == undefined) roll = 'skill';
if (ability == undefined) ability = 'str';
if (skill == undefined) skill = 'acr';
if (save == undefined) save = 'fort';
if (roll == 'perception') {
this.checkRoll(`Perception Check`, token.actor.perception, 'perception-check', token.actor);
}
if (roll == 'initiative') {
token.actor.rollInitiative({createCombatants:true, initiativeOptions: {skipDialog: true}});
}
if (roll == 'ability') return; //Ability Checks are not supported in pf2e
else if (roll == 'save') {
let ability = save;
if (ability == 'fort') ability = 'fortitude';
else if (ability == 'ref') ability = 'reflex';
else if (ability == 'will') ability = 'will';
if (token.actor.type == 'hazard' && ability == 'will') return; //Hazards don't have Will saves
let abilityName = ability.charAt(0).toUpperCase() + ability.slice(1);
this.checkRoll(`${abilityName} Saving Throw`, token.actor.saves?.[ability], 'saving-throw', token.actor);
}
else if (roll == 'skill') {
if (skill.startsWith('lor')) {
const index = parseInt(skill.split('_')[1])-1;
const loreSkills = this.getLoreSkills(token);
if (loreSkills.length > index) {
let loreSkill = loreSkills[index];
skill = loreSkill.shortform == undefined? loreSkills[index].expanded : loreSkills[index].shortform;
} else {
return;
}
}
let skillName = token.actor.data.data.skills?.[skill].name;
skillName = skillName.charAt(0).toUpperCase() + skillName.slice(1);
this.checkRoll(`Skill Check: ${skillName}`, token.actor.skills?.[skill], 'skill-check', token.actor);
}
}
checkRoll(checkLabel,stat,type,actor) {
let checkModifier = new game.pf2e.CheckModifier(checkLabel, stat);
game.pf2e.Check.roll(checkModifier, {type:type, actor: actor, skipDialog: true}, null);
}
/**
* Items
*/
getItems(token,itemType) {
if (this.isLimitedSheet(token.actor)) return [];
if (itemType == undefined) itemType = 'any';
const allItems = token.actor.items;
if (itemType == 'any') return allItems.filter(i => i.type == 'weapon' || i.type == 'equipment' || i.type == 'consumable' || i.type == 'loot' || i.type == 'container');
if (itemType == 'weapon') return allItems.filter(i => i.type == 'weapon' || i.type == 'melee') //Include melee actions for NPCs without equipment
else return allItems.filter(i => i.type == itemType);
}
getItemUses(item) {
return {available: item.quantity.value};
}
/**
* Features
*/
getFeatures(token,featureType) {
if (this.isLimitedSheet(token.actor)) return [];
if (featureType == undefined) featureType = 'any';
const allItems = token.actor.items;
if (featureType == 'any') return allItems.filter(i => i.type == 'ancestry' || i.type == 'background' || i.type == 'class' || i.type == 'feat' || i.type == 'action');
if (featureType == 'action-any') return allItems.filter(i => i.type == 'action');
if (featureType == 'action-def') return allItems.filter(i => i.type == 'action' && i.data.data.actionCategory?.value == 'defensive');
if (featureType == 'action-int') return allItems.filter(i => i.type == 'action' && i.data.data.actionCategory?.value == 'interaction');
if (featureType == 'action-off') return allItems.filter(i => i.type == 'action' && i.data.data.actionCategory?.value == 'offensive');
if (featureType == 'strike') { //Strikes are not in the actor.items collection
if (token.actor.type == 'hazard' || token.actor.type == 'familiar') {
return allItems.filter(i => i.type == 'melee' || i.type == 'ranged');
}
let actions = token.actor.data.data.actions?.filter(a=>a.type == 'strike');
for (let a of actions) {
a.img = a.imageUrl;
a.data = {
sort: 1
};
}
return actions;
}
else return allItems.filter(i => i.type == featureType)
}
getFeatureUses(item) {
if (item.data.type == 'class') return {available: item.actor.details.level.value};
else return;
}
/**
* Spells
*/
buildSpellData(token) {
let spellData = [[],[],[],[],[],[],[],[],[],[],[],[]];
let spellcastingEntries = token.actor.spellcasting;
const actorLevel = token.actor.data.data.details.level.value;
spellcastingEntries.forEach(spellCastingEntry => {
let highestSpellSlot = Math.ceil(actorLevel/2);
while (spellCastingEntry.data.data.slots?.[`slot${highestSpellSlot}`]?.max <= 0) highestSpellSlot--;
//Prepared (not flexible)
if (spellCastingEntry.data.data.prepared?.value == 'prepared' && !spellCastingEntry?.data.data?.prepared?.flexible == true) {
for (let slotLevel = 0; slotLevel < 11; slotLevel++) {
for (let slot = 0; slot < spellCastingEntry.data.data.slots?.[`slot${slotLevel}`].max; slot++) {
let spellId = spellCastingEntry.data.data.slots?.[`slot${slotLevel}`].prepared?.[slot].id;
let spell = spellCastingEntry.spells.get(spellId);
if (spellId != null) {
spellData[slotLevel].push(spell);
}
}
}
} else {
spellCastingEntry.spells.forEach( ses => {
if ((spellCastingEntry.data.data.prepared.value == 'spontaneous' || spellCastingEntry.data.data.prepared?.flexible == true) && ses.data.data.location.signature == true) {
let baseLevel = this.getSpellLevel(ses);
for (let level = baseLevel; level <= highestSpellSlot; level++) {
spellData[level].push(ses);
}
} else {
spellData[this.getSpellLevel(ses)].push(ses);
}
});
}
});
this.tokenSpellData.set(token.id, {spellData: spellData, timeStamp: Date.now()});
return spellData;
}
getSpellData(token) {
let existingSpellData = this.tokenSpellData.get(token.id);
if (existingSpellData == undefined) return this.buildSpellData(token);
let milisSinceCreation = Date.now() - existingSpellData.timeStamp;
if (milisSinceCreation > 10000) {
this.tokenSpellData.delete(token.id);
return this.buildSpellData(token);
}
return existingSpellData.spellData;
}
getSpellLevel(spell) {
if (spell.isFocusSpell == true) return 11;
if (spell.isCantrip == true) return 0;
return spell.level;
}
getSpells(token,level) {
if (this.isLimitedSheet(token.actor)) return '';
if (level == undefined) level = 'any';
let spellData = this.getSpellData(token);
if (level == 'f') return this.getUniqueSpells(spellData[11]);
if (level == 'any') return this.getUniqueSpells(spellData.flat());
return this.getUniqueSpells(spellData[level]);
}
getUniqueSpells(spells) {
return Array.from(new Set(spells));
}
getSpellUses(token,level,item) {
if (this.isLimitedSheet(token.actor)) return '';
if (level == undefined || level == 'any') level = item.level;
if (item.isCantrip == true) return;
if (item.isFocusSpell == true) return {
available: token.actor.data.data.resources.focus.value,
maximum: token.actor.data.data.resources.focus.max
}
const spellbook = this.findSpellcastingEntry(token.actor, item);
if (spellbook == undefined) return;
if (spellbook.data.data.prepared.value == 'innate') {
return {
available: item.data.data.location.uses.value,
maximum: item.data.data.location.uses.max
}
}
if (spellbook.data.data.prepared.value == 'prepared') {
if (!spellbook.data.data.prepared?.flexible == true) {
let slotsExpended = 0;
let slotsPrepared = 0;
for (let slot = 0; slot < spellbook.data.data.slots?.[`slot${level}`].max; slot++) {
let slotEntry = spellbook.data.data.slots?.[`slot${level}`].prepared?.[slot];
if (slotEntry.id == item.id) {
slotsPrepared++;
if (slotEntry?.expended == true) {
slotsExpended++;
}
}
}
return {
available: slotsPrepared - slotsExpended,
maximum: slotsPrepared
}
}
}
return {
available: spellbook.data.data.slots?.[`slot${level}`].value,
maximum: spellbook.data.data.slots?.[`slot${level}`].max
}
}
findSpellcastingEntry(actor, spell) {
let spellcastingEntries = actor.spellcasting;
let result;
spellcastingEntries.forEach(spellCastingEntry => {
if (spellCastingEntry.spells.get(spell.id) != undefined) {
result = spellCastingEntry;
}
});
return result;
}
rollItem(item, settings) {
let variant = 0;
if (otherControls.rollOption == 'map1') variant = 1;
if (otherControls.rollOption == 'map2') variant = 2;
if (item?.parent?.type == 'hazard' && item.type==='melee') return item.rollNPCAttack({}, variant+1);
if (item.type==='strike') return item.variants[variant].roll({event});
if (item?.parent?.type !== 'hazard' && (item.type==='weapon' || item.type==='melee')) return item.parent.actions.find(a=>a.name===item.name).variants[variant].roll({event});
if (item.type === 'spell') {
const spellbook = this.findSpellcastingEntry(item.parent, item);
if (spellbook != undefined) {
let spellLvl = item.level;
if (settings.spellType == 'f' || settings.spellType == '0') {
const actorLevel = item.parent.data.data.details.level.value;
spellLvl = Math.ceil(actorLevel/2);
} else if (settings.spellType != 'any') {
spellLvl = settings.spellType;
}
if (spellbook.data.data.prepared.value == 'prepared' && !spellbook.data.data.prepared?.flexible == true) {
for (let slotId = 0; slotId < spellbook.data.data.slots?.[`slot${spellLvl}`].max; slotId++) {
let slotEntry = spellbook.data.data.slots?.[`slot${spellLvl}`].prepared?.[slotId];
if (slotEntry.id == item.id) {
if (!slotEntry?.expended == true) {
return spellbook.cast(item, {slot: slotId, level: spellLvl});
}
}
}
} else {
return spellbook.cast(item, { level: spellLvl});
}
}
}
return game.pf2e.rollItemMacro(item.id);
}
isLimitedSheet(actor) {
return limitedSheets.includes(actor.type);
}
/**
* Ring Colors
*/
getSkillRingColor(token, skill) {
return this.getRingColor(this.findSkill(token, skill));
}
getSaveRingColor(token, save) {
save = this.fixSave(save);
return this.getRingColor(this.findSave(token, save));
}
getRingColor(stat) {
if (stat == undefined) return;
let statModifiers = stat?.modifiers || stat?._modifiers;
const profLevel = statModifiers?.find(m => m.type == 'proficiency')?.slug;
if (profLevel != undefined) {
return proficiencyColors?.[profLevel];
}
return;
}
}