10 Commits

Author SHA1 Message Date
CDeenen
874d9c8fb6 v1.4.6 2021-09-07 22:49:16 +02:00
Material Foundry
66b8406615 Merge pull request #81 from kyamsil/pf2e_refresh
Pf2e refresh
2021-09-07 22:24:44 +02:00
kyamsil
e6c54a2b13 Fix FlatFooted condition handling 2021-09-06 03:53:22 +01:00
kyamsil
c01f9ec566 Added support for Lore Skills, Perception and Initiative. Additional skill and saves icons 2021-08-25 23:10:39 +01:00
kyamsil
7bd06d7797 Cleanup 2021-08-03 00:51:51 +01:00
kyamsil
2fd5f81891 Adding management of actions and strikes 2021-08-03 00:42:26 +01:00
kyamsil
e8df118ce1 Defaulting to rollItemMacro. Fixes for spell and spell uses 2021-08-02 01:42:55 +01:00
kyamsil
6e22fef160 Merge branch 'Master' into pf2e_refresh 2021-08-01 22:57:28 +01:00
kyamsil
3a52ed28b4 Change the way conditions with values are handled 2021-08-01 22:48:08 +01:00
CDeenen
6c840cbf59 v1.4.5 2021-07-30 00:07:58 +01:00
27 changed files with 209 additions and 29 deletions

View File

@@ -1,4 +1,21 @@
# Changelog Material Deck Module
### v1.4.6 - 07-09-2021
Fixes:
<ul>
<li>Token Action => Move token: If the user is not the GM, tokens can no longer move if game is paused, and they can no longer move through walls</li>
<li>Modifications made in the Property Inspector now immediately get saved, instead of when user deselects the setting (changed 'onchange' to 'oninput' event)</li>
</ul>
Additions:
<ul>
<li>Playlist Action: Added a 'Pause All' option</li>
</ul>
Other:
<ul>
<li>PF2E compatibility updated (thanks @kyamsil)</li>
</ul>
### v1.4.5 - 27-07-2021
Fixes:
<ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,2 +1,3 @@
play.png: Edited from https://fontawesome.com/icons/play?style=solid
stop.png: Edited from https://fontawesome.com/icons/stop?style=solid
stop.png: Edited from https://fontawesome.com/icons/stop?style=solid
pause.png: Edited from https://fontawesome.com/v5.15/icons/pause?style=solid

BIN
img/playlist/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -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
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
img/token/abilities/ref.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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
sur.png: https://game-icons.net/1x1/delapouite/pyre.html
thi.png: https://game-icons.net/1x1/delapouite/lock-picking.html

BIN
img/token/skills/cra.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
img/token/skills/dip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
img/token/skills/occ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
img/token/skills/soc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
img/token/skills/thi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -2,7 +2,7 @@
"name": "MaterialDeck",
"title": "Material Deck",
"description": "Material Deck allows you to control Foundry using an Elgato Stream Deck",
"version": "1.4.5",
"version": "1.4.6",
"author": "CDeenen",
"authors": {
"name": "CDeenen",
@@ -22,7 +22,7 @@
],
"socket": true,
"minimumCoreVersion": "0.7.5",
"compatibleCoreVersion": "0.8.8",
"compatibleCoreVersion": "0.8.9",
"languages": [
{
"lang": "en",

View File

@@ -36,7 +36,7 @@ export class PlaylistControl{
this.updateTrack(settings,context,device);
}
else {
const src = 'modules/MaterialDeck/img/playlist/stop.png';
const src = mode == 'stopAll' ? 'modules/MaterialDeck/img/playlist/stop.png' : 'modules/MaterialDeck/img/playlist/pause.png';
const background = settings.background ? settings.background : '#000000';
const ringColor = (game.playlists.playing.length > 0) ? '#00FF00' : '#000000';
const ring = (game.playlists.playing.length > 0) ? 2 : 1;
@@ -167,6 +167,27 @@ export class PlaylistControl{
}
}
pauseAll(){
if (game.user.isGM == false) {
const payload = {
"msgType": "pauseAllPlaylists"
};
game.socket.emit(`module.MaterialDeck`, payload);
return;
}
/*
let playing = game.playlists.playing;
for (let i=0; i<playing.length; i++){
for (let sound of playing[i].sounds.contents) {
if (sound.playing) sound.sound.pause();
}
}
*/
for (let elmnt of document.getElementsByClassName('sound-control pause'))
elmnt.click();
}
getPlaylist(num){
let selectedPlaylists = game.settings.get(MODULE.moduleName,'playlists').selectedPlaylist;
if (selectedPlaylists != undefined)
@@ -191,6 +212,9 @@ export class PlaylistControl{
if (playlistMode == 'stopAll') {
this.stopAll(true);
}
else if (playlistMode == 'pauseAll') {
this.pauseAll();
}
else {
if (playlistType == 'playStop') {
let playlist = this.getPlaylist(playlistNr);

View File

@@ -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<otherSpeeds.length; i++)
speed += `\n${otherSpeeds[i].type}: ${otherSpeeds[i].value}`;
for (let os of otherSpeeds)
speed += `\n${os.type} ${os.total}'`;
return speed;
}
getInitiative(token) {
let initiative = token.actor.data.data.attributes.init.value;
return (initiative >= 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);
}
}

View File

@@ -80,7 +80,8 @@ export class TokenHelper{
let location = token.getCenter(x,y);
canvas.animatePan(location);
}
if (game.user.isGM == false && game.paused == true && (token.can(game.user,"control") == false || token.checkCollision(token.getCenter(x, y)))) return;
if (game.user.isGM == false && game.paused) return;
if (game.user.isGM == false && (token.can(game.user,"control") == false || token.checkCollision(token.getCenter(x, y)))) return;
if (compatibleCore("0.8.1")) token.document.update({x:x,y:y});
else token.update({x:x,y:y});
};
@@ -221,6 +222,11 @@ export class TokenHelper{
return this.system.getResilience(token)
}
/* PF2E */
getPerception(token) {
return this.system.getPerception(token)
}
/**
* Conditions
*/
@@ -236,6 +242,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
*/

View File

@@ -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;