From bc0804bc088a6e57fae6d1df6cd5df265e7713d5 Mon Sep 17 00:00:00 2001 From: kyamsil Date: Wed, 6 Apr 2022 21:36:48 +0100 Subject: [PATCH 1/5] Fixed rolls and conditions to work with latest pf2e version --- src/systems/pf2e.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index b013641..e70cbdd 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -142,9 +142,7 @@ export class pf2e{ 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); + await game.pf2e.ConditionManager.addConditionToToken(condition, token); } } else { try { @@ -166,19 +164,16 @@ export class pf2e{ async toggleCondition(token,condition) { if (condition == undefined) condition = 'removeAll'; if (condition == 'removeAll'){ - for( let effect of token.actor.items.filter(i => i.type == 'condition')) - await effect.delete(); + 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) { - const Condition = this.getConditionName(condition); - const newCondition = game.pf2e.ConditionManager.getCondition(Condition); - newCondition.data.sources.hud = !0, - await game.pf2e.ConditionManager.addConditionToToken(newCondition, token); + await game.pf2e.ConditionManager.addConditionToToken(condition, token); } else { - effect.delete(); + await game.pf2e.ConditionManager.removeConditionFromToken(effect.data._id, token); } } return true; @@ -188,19 +183,25 @@ export class pf2e{ * Roll */ roll(token,roll,options,ability,skill,save) { + 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') token.actor.data.data.attributes.perception.roll(options); + if (roll == 'perception') { + let checkModifier = new game.pf2e.CheckModifier(`Perception Check`, token.actor.perception); + game.pf2e.Check.roll(checkModifier, {type:"perception-check", actor: token.actor, skipDialog: true}, null); + } if (roll == 'initiative') token.actor.rollInitiative(options); - if (roll == 'ability') token.actor.rollAbility(options, ability); + 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'; - token.actor.rollSave(options, ability); + let abilityName = ability.charAt(0).toUpperCase() + ability.slice(1); + let checkModifier = new game.pf2e.CheckModifier(`${abilityName} Check`, token.actor.saves?.[ability]); + game.pf2e.Check.roll(checkModifier, {type:"saving-throw", actor: token.actor, skipDialog: true}, null); } else if (roll == 'skill') { if (skill.startsWith('lor')) { @@ -212,8 +213,12 @@ export class pf2e{ } else { return; } - } - token.actor.data.data.skills?.[skill].roll(options); + } + let skillName = token.actor.data.data.skills?.[skill].name; + skillName = skillName.charAt(0).toUpperCase() + skillName.slice(1); + let checkModifier = new game.pf2e.CheckModifier(`${skillName} Check`, token.actor.skills?.[skill]); + + game.pf2e.Check.roll(checkModifier, {type:"skill-check", actor: token.actor, skipDialog: true}, null); } } From d3c2c6465de2c7679419894f196a844158d3b624 Mon Sep 17 00:00:00 2001 From: kyamsil Date: Thu, 7 Apr 2022 03:00:51 +0100 Subject: [PATCH 2/5] Some clean up and checks for special types of actors --- src/systems/pf2e.js | 102 ++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index e70cbdd..0142095 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -1,38 +1,48 @@ import {compatibleCore} from "../misc.js"; import {otherControls} from "../../MaterialDeck.js"; +const limitedSheets = ['loot', 'vehicle']; + export class pf2e{ + constructor(){ } getHP(token) { - const hp = token.actor.data.data.attributes.hp; + const hp = token.actor.attributes?.hp; return { - value: hp.value, - max: hp.max + value: (hp?.value == null) ? 0 : hp.value, + max: (hp?.max == null) ? 0 : hp.max } } getTempHP(token) { - const hp = token.actor.data.data.attributes.hp; + const hp = token.actor.attributes?.hp; return { - value: (hp.temp == null) ? 0 : hp.temp, - max: (hp.tempmax == null) ? 0 : hp.tempmax + value: (hp?.temp == null) ? 0 : hp.temp, + max: (hp?.tempmax == null) ? 0 : hp.tempmax } } getAC(token) { - return token.actor.data.data.attributes.ac.value; + const ac = token.actor.attributes?.ac; + return (ac?.value == null) ? 10 : ac?.value; } getShieldHP(token) { - return token.actor.data.data.attributes.shield.value; + const shieldhp = token.actor.attributes.shield + return (shieldhp?.value == null) ? 0 : shieldhp?.value; } getSpeed(token) { - let speed = `${token.actor.data.data.attributes.speed.total}'`; - const otherSpeeds = token.actor.data.data.attributes.speed.otherSpeeds; + 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}'`; @@ -40,14 +50,20 @@ export class pf2e{ } getInitiative(token) { - let initiativeModifier = token.actor.data.data.attributes?.initiative.totalModifier; - let initiativeAbility = token.actor.data.data.attributes?.initiative.ability; + 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 (initiativeAbility != '') ? `(${initiativeAbility}): ${initiativeModifier}` : `(perception): ${initiativeModifier}`; + return `${initiativeLabel} (${initiativeModifier})`; } toggleInitiative(token) { @@ -63,31 +79,36 @@ export class pf2e{ } getPerception(token) { - let perception = token.actor.data.data.attributes?.perception.totalModifier; + 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.data.data.abilities?.[ability].value; + 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.data.data.abilities?.[ability].mod; + let val = token.actor.abilities?.[ability]?.mod; return (val >= 0) ? `+${val}` : val; } getAbilitySave(token, ability) { + if (this.isLimitedSheet(token.actor)) return ''; if (ability == undefined) ability = 'fortitude'; else if (ability == 'fort') ability = 'fortitude'; else if (ability == 'ref') ability = 'reflex'; else if (ability == 'will') ability = 'will'; - let val = token.actor.data.data.saves?.[ability].value; + let val = token.actor.data.data.saves?.[ability]?.value; return (val >= 0) ? `+${val}` : val; } getSkill(token, skill) { + if (this.isLimitedSheet(token.actor)) return ''; if (skill == undefined) skill = 'acr'; if (skill.startsWith('lor')) { const index = parseInt(skill.split('_')[1])-1; @@ -103,6 +124,7 @@ export class pf2e{ } 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); } @@ -183,25 +205,28 @@ export class pf2e{ * 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') { - let checkModifier = new game.pf2e.CheckModifier(`Perception Check`, token.actor.perception); - game.pf2e.Check.roll(checkModifier, {type:"perception-check", actor: token.actor, skipDialog: true}, null); + this.checkRoll(`Perception Check`, token.actor.perception, 'perception-check', token.actor); } - if (roll == 'initiative') token.actor.rollInitiative(options); + if (roll == 'initiative') { + this.checkRoll(token.actor.attributes?.initiative?.label, token.actor.attributes?.initiative, 'initiative', token.actor); + } + 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); - let checkModifier = new game.pf2e.CheckModifier(`${abilityName} Check`, token.actor.saves?.[ability]); - game.pf2e.Check.roll(checkModifier, {type:"saving-throw", actor: token.actor, skipDialog: true}, null); + this.checkRoll(`${abilityName} Saving Throw`, token.actor.saves?.[ability], 'saving-throw', token.actor); } else if (roll == 'skill') { if (skill.startsWith('lor')) { @@ -216,16 +241,20 @@ export class pf2e{ } let skillName = token.actor.data.data.skills?.[skill].name; skillName = skillName.charAt(0).toUpperCase() + skillName.slice(1); - let checkModifier = new game.pf2e.CheckModifier(`${skillName} Check`, token.actor.skills?.[skill]); - - game.pf2e.Check.roll(checkModifier, {type:"skill-check", actor: token.actor, skipDialog: true}, null); + 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'); @@ -234,13 +263,14 @@ export class pf2e{ } getItemUses(item) { - return {available: item.data.data.quantity.value}; + 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'); @@ -249,7 +279,10 @@ export class pf2e{ 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'); + 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 = { @@ -262,7 +295,7 @@ export class pf2e{ } getFeatureUses(item) { - if (item.data.type == 'class') return {available: item.actor.data.data.details.level.value}; + if (item.data.type == 'class') return {available: item.actor.details.level.value}; else return; } @@ -270,6 +303,7 @@ export class pf2e{ * Spells */ getSpells(token,level) { + if (this.isLimitedSheet(token.actor)) return ''; if (level == undefined) level = 'any'; const allItems = token.actor.items; if (level == 'any') return allItems.filter(i => i.type == 'spell') @@ -278,13 +312,14 @@ export class pf2e{ } getSpellUses(token,level,item) { + if (this.isLimitedSheet(token.actor)) 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 { - available: spellbook.data.data.slots?.[`slot${level}`].value, - maximum: spellbook.data.data.slots?.[`slot${level}`].max + available: spellbook.slots?.[`slot${level}`].value, + maximum: spellbook.slots?.[`slot${level}`].max } } @@ -292,8 +327,13 @@ export class pf2e{ 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.type==='weapon' || item.type==='melee') return item.parent.data.data.actions.find(a=>a.name===item.name).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}); return game.pf2e.rollItemMacro(item.id); } + + isLimitedSheet(actor) { + return limitedSheets.includes(actor.type); + } } \ No newline at end of file From ccacd3e26e0a74182ba9cde532628e9546533143 Mon Sep 17 00:00:00 2001 From: kyamsil Date: Thu, 7 Apr 2022 23:03:48 +0100 Subject: [PATCH 3/5] Changed initiative rolling so it adds to combat without dialog --- src/systems/pf2e.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index 0142095..4953fe6 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -215,7 +215,7 @@ export class pf2e{ this.checkRoll(`Perception Check`, token.actor.perception, 'perception-check', token.actor); } if (roll == 'initiative') { - this.checkRoll(token.actor.attributes?.initiative?.label, token.actor.attributes?.initiative, 'initiative', token.actor); + token.actor.rollInitiative({createCombatants:true, initiativeOptions: {skipDialog: true}}); } if (roll == 'ability') return; //Ability Checks are not supported in pf2e From 4d320a5f6c5c08d4ef382a5da3e68ea61c5c7259 Mon Sep 17 00:00:00 2001 From: kyamsil Date: Fri, 8 Apr 2022 20:26:33 +0100 Subject: [PATCH 4/5] Added color ring based on proficiency levels for skills and saves (pf2e and dnd5e) --- img/token/skills/SOURCES.txt | 1 + img/token/skills/lor.png | Bin 0 -> 30367 bytes src/systems/demonlord.js | 11 +++++ src/systems/dnd35e.js | 11 +++++ src/systems/dnd5e.js | 22 ++++++++++ src/systems/forbidden-lands.js | 11 +++++ src/systems/pf2e.js | 74 ++++++++++++++++++++++++++++----- src/systems/tokenHelper.js | 10 +++++ src/systems/wfrp4e.js | 9 ++++ src/token.js | 16 ++++--- 10 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 img/token/skills/lor.png diff --git a/img/token/skills/SOURCES.txt b/img/token/skills/SOURCES.txt index a3317cf..28ac895 100644 --- a/img/token/skills/SOURCES.txt +++ b/img/token/skills/SOURCES.txt @@ -10,6 +10,7 @@ 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 +lor.png: https://game-icons.net/1x1/lorc/bookmarklet.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 diff --git a/img/token/skills/lor.png b/img/token/skills/lor.png new file mode 100644 index 0000000000000000000000000000000000000000..4d25e52f19a2d94674d1bd8f01cb35cd390dd616 GIT binary patch literal 30367 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 CQ9>yI literal 0 HcmV?d00001 diff --git a/src/systems/demonlord.js b/src/systems/demonlord.js index 2cabe4a..985ac49 100644 --- a/src/systems/demonlord.js +++ b/src/systems/demonlord.js @@ -134,4 +134,15 @@ export class demonlord{ rollItem(item) { return item.roll() } + + /** + * Ring Colors + */ + getSkillRingColor(token, skill) { + return; + } + + getSaveRingColor(token, save) { + return; + } } \ No newline at end of file diff --git a/src/systems/dnd35e.js b/src/systems/dnd35e.js index 32d1604..72bdc75 100644 --- a/src/systems/dnd35e.js +++ b/src/systems/dnd35e.js @@ -199,4 +199,15 @@ export class dnd35e{ rollItem(item) { return item.roll() } + + /** + * Ring Colors + */ + getSkillRingColor(token, skill) { + return; + } + + getSaveRingColor(token, save) { + return; + } } \ No newline at end of file diff --git a/src/systems/dnd5e.js b/src/systems/dnd5e.js index b32ee24..f0c27b4 100644 --- a/src/systems/dnd5e.js +++ b/src/systems/dnd5e.js @@ -1,5 +1,12 @@ import {compatibleCore} from "../misc.js"; +const proficiencyColors = { + 0: "#000000", + 0.5: "#804A00", + 1: "#C0C0C0", + 2: "#FFD700" +} + export class dnd5e{ constructor(){ @@ -202,4 +209,19 @@ export class dnd5e{ rollItem(item) { return item.roll() } + + /** + * Ring Colors + */ + getSkillRingColor(token, skill) { + const profLevel = token.actor.data.data?.skills[skill]?.proficient; + if (profLevel == undefined) return; + return proficiencyColors?.[profLevel]; + } + + getSaveRingColor(token, save) { + const profLevel = token.actor.data.data?.abilities[save]?.proficient; + if (profLevel == undefined) return; + return proficiencyColors?.[profLevel]; + } } \ No newline at end of file diff --git a/src/systems/forbidden-lands.js b/src/systems/forbidden-lands.js index 0eb1a00..98025a1 100644 --- a/src/systems/forbidden-lands.js +++ b/src/systems/forbidden-lands.js @@ -235,4 +235,15 @@ export class forbiddenlands{ sheet.rollSpecificAttack(item.id); return item.sendToChat(); } + + /** + * Ring Colors + */ + getSkillRingColor(token, skill) { + return; + } + + getSaveRingColor(token, save) { + return; + } } \ No newline at end of file diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index 4953fe6..321b42a 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -2,6 +2,14 @@ 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{ @@ -98,29 +106,50 @@ export class pf2e{ } getAbilitySave(token, ability) { - if (this.isLimitedSheet(token.actor)) return ''; - if (ability == undefined) ability = 'fortitude'; - else if (ability == 'fort') ability = 'fortitude'; - else if (ability == 'ref') ability = 'reflex'; - else if (ability == 'will') ability = 'will'; - let val = token.actor.data.data.saves?.[ability]?.value; + 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) { - if (this.isLimitedSheet(token.actor)) return ''; + 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].name}: +${loreSkills[index].totalModifier}`; + return loreSkills[index]; } else { - return ''; + return; } } - const val = token.actor.data.data.skills?.[skill].totalModifier; - return (val >= 0) ? `+${val}` : val; + return token.actor.data.data.skills?.[skill]; } getLoreSkills(token) { @@ -336,4 +365,27 @@ export class pf2e{ 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; + // console.log(`Proficiency Level for ${stat.name}: ${profLevel}`); + if (profLevel != undefined) { + return proficiencyColors?.[profLevel]; + } + return; + } } \ No newline at end of file diff --git a/src/systems/tokenHelper.js b/src/systems/tokenHelper.js index eed2f70..aa4c5ad 100644 --- a/src/systems/tokenHelper.js +++ b/src/systems/tokenHelper.js @@ -318,4 +318,14 @@ export class TokenHelper{ rollItem(item) { return this.system.rollItem(item); } + + /** + * Ring Colors + */ + getSkillRingColor(token,skill) { + return this.system.getSkillRingColor(token,skill); + } + getSaveRingColor(token,save) { + return this.system.getSaveRingColor(token,save); + } } \ No newline at end of file diff --git a/src/systems/wfrp4e.js b/src/systems/wfrp4e.js index eefaee2..7b62cc0 100644 --- a/src/systems/wfrp4e.js +++ b/src/systems/wfrp4e.js @@ -168,5 +168,14 @@ export class wfrp4e { return; } + /** + * Ring Colors + */ + getSkillRingColor(token, skill) { + return; + } + getSaveRingColor(token, save) { + return; + } } \ No newline at end of file diff --git a/src/token.js b/src/token.js index abb2765..a9da86b 100644 --- a/src/token.js +++ b/src/token.js @@ -198,8 +198,16 @@ export class TokenControl{ else if (stats == 'PassiveInvestigation') txt += tokenHelper.getPassiveInvestigation(token); else if (stats == 'Ability') txt += tokenHelper.getAbility(token, settings.ability); else if (stats == 'AbilityMod') txt += tokenHelper.getAbilityModifier(token, settings.ability); - else if (stats == 'Save') txt += tokenHelper.getAbilitySave(token, settings.save); - else if (stats == 'Skill') txt += tokenHelper.getSkill(token, settings.skill); + else if (stats == 'Save') { + txt += tokenHelper.getAbilitySave(token, settings.save); + ringColor = tokenHelper.getSaveRingColor(token, settings.save); + if (ringColor != undefined) ring = 2; + } + else if (stats == 'Skill') { + txt += tokenHelper.getSkill(token, settings.skill); + ringColor = tokenHelper.getSkillRingColor(token, settings.skill); + if (ringColor != undefined) ring = 2; + } else if (stats == 'Prof') txt += tokenHelper.getProficiency(token); else if (stats == 'Fate') txt += tokenHelper.getFate(token) /* WFRP4e */ else if (stats == 'Fortune') txt += tokenHelper.getFortune(token) /* WFRP4e */ @@ -444,7 +452,6 @@ export class TokenControl{ } else if (stats == 'Ability' || stats == 'AbilityMod' || stats == 'Save') { overlay = true; - ring = 1; let ability = (stats == 'Save') ? settings.save : settings.ability; if (ability == undefined) ability = 'str'; if (ability == 'con') iconSrc = "modules/MaterialDeck/img/token/abilities/cons.png"; @@ -452,10 +459,9 @@ export class TokenControl{ } else if (stats == 'Skill') { overlay = true; - ring = 1; let skill = settings.skill; if (skill == undefined) skill = 'acr'; - else iconSrc = "modules/MaterialDeck/img/token/skills/" + skill + ".png"; + else iconSrc = "modules/MaterialDeck/img/token/skills/" + (skill.startsWith('lor')? 'lor' : skill) + ".png"; } else if (settings.onClick == 'center' || settings.onClick == 'centerSelect') { overlay = true; From 108c955ac2735803760783100e0e3c96145e93e2 Mon Sep 17 00:00:00 2001 From: kyamsil Date: Mon, 11 Apr 2022 02:31:35 +0100 Subject: [PATCH 5/5] Revamped Spellbook handling and clicking for all types --- src/systems/pf2e.js | 140 ++++++++++++++++++++++++++++++++++--- src/systems/tokenHelper.js | 4 +- src/token.js | 2 +- 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/src/systems/pf2e.js b/src/systems/pf2e.js index 321b42a..bb45f21 100644 --- a/src/systems/pf2e.js +++ b/src/systems/pf2e.js @@ -17,6 +17,8 @@ export class pf2e{ } + tokenSpellData = new Map(); + getHP(token) { const hp = token.actor.attributes?.hp; return { @@ -331,34 +333,155 @@ export class pf2e{ /** * 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'; - const allItems = token.actor.items; - if (level == 'any') return allItems.filter(i => i.type == 'spell') - 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) + 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; - const spellbook = token.actor.items.filter(i => i.data.type === 'spellcastingEntry')[0]; + 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.slots?.[`slot${level}`].value, - maximum: spellbook.slots?.[`slot${level}`].max + available: spellbook.data.data.slots?.[`slot${level}`].value, + maximum: spellbook.data.data.slots?.[`slot${level}`].max } } - rollItem(item) { + 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); } @@ -382,7 +505,6 @@ export class pf2e{ if (stat == undefined) return; let statModifiers = stat?.modifiers || stat?._modifiers; const profLevel = statModifiers?.find(m => m.type == 'proficiency')?.slug; - // console.log(`Proficiency Level for ${stat.name}: ${profLevel}`); if (profLevel != undefined) { return proficiencyColors?.[profLevel]; } diff --git a/src/systems/tokenHelper.js b/src/systems/tokenHelper.js index aa4c5ad..0bfa835 100644 --- a/src/systems/tokenHelper.js +++ b/src/systems/tokenHelper.js @@ -315,8 +315,8 @@ export class TokenHelper{ return this.system.getSpellUses(token,level,item); } - rollItem(item) { - return this.system.rollItem(item); + rollItem(item, settings) { + return this.system.rollItem(item, settings); } /** diff --git a/src/token.js b/src/token.js index a9da86b..84b585c 100644 --- a/src/token.js +++ b/src/token.js @@ -841,7 +841,7 @@ export class TokenControl{ const item = items[itemNr]; if (item != undefined) { - tokenHelper.rollItem(item); + tokenHelper.rollItem(item, settings); } }