diff --git a/img/token/abilities/SOURCES.txt b/img/token/abilities/SOURCES.txt index 583383b..7d97530 100644 --- a/img/token/abilities/SOURCES.txt +++ b/img/token/abilities/SOURCES.txt @@ -4,4 +4,7 @@ dex.png: https://game-icons.net/1x1/darkzaitzev/acrobatic.html cons.png: https://game-icons.net/1x1/zeromancer/heart-plus.html int.png: https://game-icons.net/1x1/lorc/bookmarklet.html wis.png: https://game-icons.net/1x1/delapouite/wisdom.html -cha.png: https://game-icons.net/1x1/lorc/icicles-aura.html \ No newline at end of file +cha.png: https://game-icons.net/1x1/lorc/icicles-aura.html +fort.png: https://game-icons.net/1x1/delapouite/rock-golem.html +ref.png: https://game-icons.net/1x1/lorc/dodging.html +will.png: https://game-icons.net/1x1/lorc/meditation.html \ No newline at end of file diff --git a/img/token/abilities/fort.png b/img/token/abilities/fort.png new file mode 100644 index 0000000..33dee76 Binary files /dev/null and b/img/token/abilities/fort.png differ diff --git a/img/token/abilities/ref.png b/img/token/abilities/ref.png new file mode 100644 index 0000000..c78ce39 Binary files /dev/null and b/img/token/abilities/ref.png differ diff --git a/img/token/abilities/will.png b/img/token/abilities/will.png new file mode 100644 index 0000000..e78273c Binary files /dev/null and b/img/token/abilities/will.png differ diff --git a/img/token/skills/SOURCES.txt b/img/token/skills/SOURCES.txt index 0d2fb22..a3317cf 100644 --- a/img/token/skills/SOURCES.txt +++ b/img/token/skills/SOURCES.txt @@ -3,17 +3,22 @@ acr.png: https://game-icons.net/1x1/delapouite/contortionist.html ani.png: https://game-icons.net/1x1/delapouite/cavalry.html arc.png: https://game-icons.net/1x1/delapouite/spell-book.html ath.png: https://game-icons.net/1x1/lorc/muscle-up.html +cra.png: https://game-icons.net/1x1/lorc/sword-smithing.html dec.png: https://game-icons.net/1x1/delapouite/convince.html +dip.png: https://game-icons.net/1x1/delapouite/shaking-hands.html his.png: https://game-icons.net/1x1/delapouite/backward-time.html ins.png: https://game-icons.net/1x1/lorc/light-bulb.html itm.png: https://game-icons.net/1x1/lorc/one-eyed.html inv.png: https://game-icons.net/1x1/lorc/magnifying-glass.html med.png: https://game-icons.net/1x1/delapouite/first-aid-kit.html nat.png: https://game-icons.net/1x1/delapouite/forest.html +occ.png: https://game-icons.net/1x1/skoll/pentacle.html prc.png: https://game-icons.net/1x1/lorc/semi-closed-eye.html prf.png: https://game-icons.net/1x1/lorc/sing.html per.png: https://game-icons.net/1x1/delapouite/public-speaker.html rel.png: https://game-icons.net/1x1/lorc/holy-grail.html +soc.png: https://game-icons.net/1x1/delapouite/trumpet-flag.html slt.png: https://game-icons.net/1x1/lorc/snatch.html ste.png: https://game-icons.net/1x1/lorc/cloak-dagger.html -sur.png: https://game-icons.net/1x1/delapouite/pyre.html \ No newline at end of file +sur.png: https://game-icons.net/1x1/delapouite/pyre.html +thi.png: https://game-icons.net/1x1/delapouite/lock-picking.html \ No newline at end of file diff --git a/img/token/skills/cra.png b/img/token/skills/cra.png new file mode 100644 index 0000000..0b1115e Binary files /dev/null and b/img/token/skills/cra.png differ diff --git a/img/token/skills/dip.png b/img/token/skills/dip.png new file mode 100644 index 0000000..ed17945 Binary files /dev/null and b/img/token/skills/dip.png differ diff --git a/img/token/skills/occ.png b/img/token/skills/occ.png new file mode 100644 index 0000000..d92dda5 Binary files /dev/null and b/img/token/skills/occ.png differ diff --git a/img/token/skills/soc.png b/img/token/skills/soc.png new file mode 100644 index 0000000..cf85e97 Binary files /dev/null and b/img/token/skills/soc.png differ diff --git a/img/token/skills/thi.png b/img/token/skills/thi.png new file mode 100644 index 0000000..ac12c64 Binary files /dev/null and b/img/token/skills/thi.png differ diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index 21ee59f..b013641 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -1,4 +1,5 @@ import {compatibleCore} from "../misc.js"; +import {otherControls} from "../../MaterialDeck.js"; export class pf2e{ constructor(){ @@ -30,17 +31,23 @@ export class pf2e{ } getSpeed(token) { - let speed = token.actor.data.data.attributes.speed.breakdown; + let speed = `${token.actor.data.data.attributes.speed.total}'`; const otherSpeeds = token.actor.data.data.attributes.speed.otherSpeeds; if (otherSpeeds.length > 0) - for (let i=0; i= 0) ? `+${initiative}` : initiative; + let initiativeModifier = token.actor.data.data.attributes?.initiative.totalModifier; + let initiativeAbility = token.actor.data.data.attributes?.initiative.ability; + if (initiativeModifier > 0) { + initiativeModifier = `+${initiativeModifier}`; + } else { + initiativeModifier = this.getPerception(token); //NPCs won't have a valid Initiative value, so default to use Perception + } + return (initiativeAbility != '') ? `(${initiativeAbility}): ${initiativeModifier}` : `(perception): ${initiativeModifier}`; } toggleInitiative(token) { @@ -55,6 +62,11 @@ export class pf2e{ return; } + getPerception(token) { + let perception = token.actor.data.data.attributes?.perception.totalModifier; + return (perception >= 0) ? `+${perception}` : perception; + } + getAbility(token, ability) { if (ability == undefined) ability = 'str'; return token.actor.data.data.abilities?.[ability].value; @@ -77,17 +89,31 @@ export class pf2e{ getSkill(token, skill) { 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].name}: +${loreSkills[index].totalModifier}`; + } else { + return ''; + } + } const val = token.actor.data.data.skills?.[skill].totalModifier; return (val >= 0) ? `+${val}` : val; } + getLoreSkills(token) { + 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 = condition.charAt(0).toUpperCase() + condition.slice(1); + const Condition = this.getConditionName(condition); const effects = token.actor.items.filter(i => i.type == 'condition'); return effects.find(e => e.name === Condition); } @@ -102,6 +128,41 @@ export class pf2e{ 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) { + const Condition = this.getConditionName(condition); + const newCondition = game.pf2e.ConditionManager.getCondition(Condition); + await game.pf2e.ConditionManager.addConditionToToken(newCondition, 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'){ @@ -111,7 +172,7 @@ export class pf2e{ else { const effect = this.getCondition(token,condition); if (effect == undefined) { - const Condition = condition.charAt(0).toUpperCase() + condition.slice(1); + const Condition = this.getConditionName(condition); const newCondition = game.pf2e.ConditionManager.getCondition(Condition); newCondition.data.sources.hud = !0, await game.pf2e.ConditionManager.addConditionToToken(newCondition, token); @@ -127,20 +188,33 @@ export class pf2e{ * Roll */ roll(token,roll,options,ability,skill,save) { - if (roll == undefined) roll = 'ability'; + if (roll == undefined) roll = 'skill'; if (ability == undefined) ability = 'str'; if (skill == undefined) skill = 'acr'; if (save == undefined) save = 'fort'; - - if (roll == 'ability') token.actor.data.data.abilities?.[ability].roll(options); + if (roll == 'perception') token.actor.data.data.attributes.perception.roll(options); + if (roll == 'initiative') token.actor.rollInitiative(options); + if (roll == 'ability') token.actor.rollAbility(options, ability); else if (roll == 'save') { let ability = save; if (ability == 'fort') ability = 'fortitude'; else if (ability == 'ref') ability = 'reflex'; else if (ability == 'will') ability = 'will'; - token.actor.data.data.saves?.[ability].roll(options); + token.actor.rollSave(options, ability); + } + 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; + } + } + token.actor.data.data.skills?.[skill].roll(options); } - else if (roll == 'skill') token.actor.data.data.skills?.[skill].roll(options); } /** @@ -150,6 +224,7 @@ export class pf2e{ 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); } @@ -163,7 +238,21 @@ export class pf2e{ getFeatures(token,featureType) { if (featureType == undefined) featureType = 'any'; const allItems = token.actor.items; - if (featureType == 'any') return allItems.filter(i => i.type == 'class' || i.type == 'feat') + 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 + 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) } @@ -179,12 +268,13 @@ export class pf2e{ if (level == undefined) level = 'any'; const allItems = token.actor.items; if (level == 'any') return allItems.filter(i => i.type == 'spell') - else return allItems.filter(i => i.type == 'spell' && i.data.data.level.value == level) + if (level == '0') return allItems.filter(i => i.type == 'spell' && i.isCantrip == true) + else return allItems.filter(i => i.type == 'spell' && i.level == level && i.isCantrip == false) } getSpellUses(token,level,item) { - if (level == undefined) level = 'any'; - if (item.data.data.level.value == 0) return; + if (level == undefined || level == 'any') level = item.level; + if (item.isCantrip == true) return; const spellbook = token.actor.items.filter(i => i.data.type === 'spellcastingEntry')[0]; if (spellbook == undefined) return; return { @@ -194,6 +284,11 @@ export class pf2e{ } rollItem(item) { - return item.roll() + let variant = 0; + if (otherControls.rollOption == 'map1') variant = 1; + if (otherControls.rollOption == 'map2') variant = 2; + if (item.type==='strike') return item.variants[variant].roll({event}); + if (item.type==='weapon' || item.type==='melee') return item.parent.data.data.actions.find(a=>a.name===item.name).variants[variant].roll({event}); + return game.pf2e.rollItemMacro(item.id); } } \ No newline at end of file diff --git a/src/systems/tokenHelper.js b/src/systems/tokenHelper.js index aad53eb..72e44f0 100644 --- a/src/systems/tokenHelper.js +++ b/src/systems/tokenHelper.js @@ -221,6 +221,11 @@ export class TokenHelper{ return this.system.getResilience(token) } + /* PF2E */ + getPerception(token) { + return this.system.getPerception(token) + } + /** * Conditions */ @@ -236,6 +241,16 @@ export class TokenHelper{ return this.system.toggleCondition(token,condition); } + /* PF2E */ + getConditionValue(token,condition) { + return this.system.getConditionValue(token,condition); + } + + /* PF2E */ + modifyConditionValue(token,condition,delta) { + return this.system.modifyConditionValue(token,condition,delta); + } + /** * Roll */ diff --git a/src/token.js b/src/token.js index b89cc50..617644a 100644 --- a/src/token.js +++ b/src/token.js @@ -153,7 +153,13 @@ export class TokenControl{ else if (stats == 'Advantage') txt += tokenHelper.getAdvantage(token) /* WFRP4e */ else if (stats == 'Resolve') txt += tokenHelper.getResolve(token) /* WFRP4e */ else if (stats == 'Resilience') txt += tokenHelper.getResilience(token) /* WFRP4e */ - + else if (stats == 'Perception') txt += tokenHelper.getPerception(token) /* PF2E */ + else if (stats == 'Condition') { /* PF2E */ + const valuedCondition = tokenHelper.getConditionValue(token, settings.condition); + if (valuedCondition != undefined) { + txt += valuedCondition?.value; + } + } if (settings.onClick == 'visibility') { //toggle visibility if (MODULE.getPermission('TOKEN','VISIBILITY') == false ) { @@ -195,7 +201,7 @@ export class TokenControl{ iconSrc = "fas fa-bullseye"; } } - else if (settings.onClick == 'condition') { //toggle condition + else if (settings.onClick == 'condition') { //handle condition if (MODULE.getPermission('TOKEN','CONDITIONS') == false ) { streamDeck.noPermission(context,device); return; @@ -502,10 +508,23 @@ export class TokenControl{ else if (onClick == 'target') { //Target token token.setTarget(!token.isTargeted,{releaseOthers:false}); } - else if (onClick == 'condition') { //Toggle condition + else if (onClick == 'condition') { //Handle condition if (MODULE.getPermission('TOKEN','CONDITIONS') == false ) return; - await tokenHelper.toggleCondition(token,settings.condition); - this.update(tokenId); + const func = settings.conditionFunction ? settings.conditionFunction : 'toggle'; + + if (func == 'toggle'){ //toggle + await tokenHelper.toggleCondition(token,settings.condition); + this.update(tokenId); + } + else if (func == 'increase'){ //increase + await tokenHelper.modifyConditionValue(token, settings.condition, +1) + this.update(tokenId); + } + else if (func == 'decrease'){ //decrease + await tokenHelper.modifyConditionValue(token, settings.condition, -1) + this.update(tokenId); + } + } else if (onClick == 'cubCondition') { //Combat Utility Belt conditions if (MODULE.getPermission('TOKEN','CONDITIONS') == false ) return;