mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-04-21 23:13:39 +02:00
Merge branch 'v14-Dev' into v14/effect-stacking
This commit is contained in:
commit
b2a900db16
149 changed files with 3336 additions and 1535 deletions
|
|
@ -43,6 +43,7 @@ CONFIG.Item.dataModels = models.items.config;
|
|||
|
||||
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
||||
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
||||
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects };
|
||||
|
||||
CONFIG.Combat.documentClass = documents.DhpCombat;
|
||||
CONFIG.Combat.dataModels = { base: models.DhCombat };
|
||||
|
|
@ -211,6 +212,7 @@ Hooks.once('init', () => {
|
|||
SYSTEM.id,
|
||||
applications.sheetConfigs.ActiveEffectConfig,
|
||||
{
|
||||
types: ['base', 'beastform', 'horde'],
|
||||
makeDefault: true,
|
||||
label: sheetLabel('DOCUMENT.ActiveEffect')
|
||||
}
|
||||
|
|
@ -268,7 +270,6 @@ Hooks.on('setup', () => {
|
|||
...damageThresholds,
|
||||
'proficiency',
|
||||
'evasion',
|
||||
'armorScore',
|
||||
'scars',
|
||||
'levelData.level.current'
|
||||
]
|
||||
|
|
@ -402,6 +403,17 @@ Hooks.on('chatMessage', (_, message) => {
|
|||
}
|
||||
});
|
||||
|
||||
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
|
||||
if (data.openForAllPlayers && data.partyId) {
|
||||
const party = game.actors.get(data.partyId);
|
||||
if (!party) return;
|
||||
|
||||
const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party);
|
||||
dialog.tabGroups.application = 'tagTeamRoll';
|
||||
await dialog.render({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const updateActorsRangeDependentEffects = async token => {
|
||||
const rangeMeasurement = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
|
|
|
|||
58
lang/en.json
58
lang/en.json
|
|
@ -452,7 +452,7 @@
|
|||
"text": "Are you sure you want to delete {name}?"
|
||||
},
|
||||
"DamageReduction": {
|
||||
"armorMarks": "Armor Marks",
|
||||
"maxUseableArmor": "Useable Armor Slots",
|
||||
"armorWithStress": "Spend 1 stress to use an extra mark",
|
||||
"thresholdImmunities": "Threshold Immunities",
|
||||
"stress": "Stress",
|
||||
|
|
@ -678,16 +678,35 @@
|
|||
},
|
||||
"TagTeamSelect": {
|
||||
"title": "Tag Team Roll",
|
||||
"FIELDS": {
|
||||
"initiator": {
|
||||
"memberId": { "label": "Initiating Character" },
|
||||
"cost": { "label": "Initiation Cost" }
|
||||
}
|
||||
},
|
||||
"leaderTitle": "Initiating Character",
|
||||
"membersTitle": "Participants",
|
||||
"partyTeam": "Party Team",
|
||||
"hopeCost": "Hope Cost",
|
||||
"initiatingCharacter": "Initiating Character",
|
||||
"selectParticipants": "Select the two participants",
|
||||
"startTagTeamRoll": "Start Tag Team Roll",
|
||||
"openDialogForAll": "Open Dialog For All",
|
||||
"rollType": "Roll Type",
|
||||
"makeYourRoll": "Make your roll",
|
||||
"cancelTagTeamRoll": "Cancel Tag Team Roll",
|
||||
"finishTagTeamRoll": "Finish Tag Team Roll",
|
||||
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
|
||||
"damageNotRolled": "Damage not rolled in chat message yet",
|
||||
"insufficientHope": "The initiating character doesn't have enough hope",
|
||||
"createTagTeam": "Create TagTeam Roll",
|
||||
"chatMessageRollTitle": "Roll"
|
||||
"createTagTeam": "Create Tag Team Roll",
|
||||
"chatMessageRollTitle": "Roll",
|
||||
"cancelConfirmTitle": "Cancel Tag Team Roll",
|
||||
"cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.",
|
||||
"hints": {
|
||||
"completeRolls": "Set up and complete the rolls for the characters",
|
||||
"selectRoll": "Select which roll value to be used for the Tag Team"
|
||||
}
|
||||
},
|
||||
"TokenConfig": {
|
||||
"actorSizeUsed": "Actor size is set, determining the dimensions"
|
||||
|
|
@ -776,6 +795,11 @@
|
|||
"bruiser": "for each Bruiser adversary.",
|
||||
"solo": "for each Solo adversary."
|
||||
},
|
||||
"ArmorInteraction": {
|
||||
"none": { "label": "Ignores Armor" },
|
||||
"active": { "label": "Active w/ Armor" },
|
||||
"inactive": { "label": "Inactive w/ Armor" }
|
||||
},
|
||||
"ArmorFeature": {
|
||||
"burning": {
|
||||
"name": "Burning",
|
||||
|
|
@ -1231,6 +1255,11 @@
|
|||
"selectType": "Select Action Type",
|
||||
"selectAction": "Action Selection"
|
||||
},
|
||||
"TagTeamRollTypes": {
|
||||
"trait": "Trait",
|
||||
"ability": "Ability",
|
||||
"damageAbility": "Damage Ability"
|
||||
},
|
||||
"TargetTypes": {
|
||||
"any": "Any",
|
||||
"friendly": "Friendly",
|
||||
|
|
@ -1859,6 +1888,17 @@
|
|||
"name": "Healing Roll"
|
||||
}
|
||||
},
|
||||
"ChangeTypes": {
|
||||
"armor": {
|
||||
"newArmorEffect": "Armor Effect",
|
||||
"FIELDS": {
|
||||
"interaction": {
|
||||
"label": "Armor Interaction",
|
||||
"hint": "Does the character wearing armor suppress this effect?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Duration": {
|
||||
"passive": "Passive",
|
||||
"temporary": "Temporary"
|
||||
|
|
@ -1883,6 +1923,10 @@
|
|||
}
|
||||
},
|
||||
"GENERAL": {
|
||||
"Ability": {
|
||||
"single": "Ability",
|
||||
"plural": "Abilities"
|
||||
},
|
||||
"Action": {
|
||||
"single": "Action",
|
||||
"plural": "Actions"
|
||||
|
|
@ -2264,6 +2308,7 @@
|
|||
"duality": "Duality",
|
||||
"dualityDice": "Duality Dice",
|
||||
"dualityRoll": "Duality Roll",
|
||||
"effect": "Effect",
|
||||
"enabled": "Enabled",
|
||||
"evasion": "Evasion",
|
||||
"equipment": "Equipment",
|
||||
|
|
@ -2336,6 +2381,10 @@
|
|||
"rerolled": "Rerolled",
|
||||
"rerollThing": "Reroll {thing}",
|
||||
"resource": "Resource",
|
||||
"result": {
|
||||
"single": "Result",
|
||||
"plural": "Results"
|
||||
},
|
||||
"roll": "Roll",
|
||||
"rollAll": "Roll All",
|
||||
"rollDamage": "Roll Damage",
|
||||
|
|
@ -3068,6 +3117,9 @@
|
|||
"knowTheTide": "Know The Tide gained a token",
|
||||
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
|
||||
},
|
||||
"Progress": {
|
||||
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
|
||||
},
|
||||
"Sidebar": {
|
||||
"actorDirectory": {
|
||||
"tier": "Tier {tier} {type}",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
updateIsAdvantage: this.updateIsAdvantage,
|
||||
selectExperience: this.selectExperience,
|
||||
toggleReaction: this.toggleReaction,
|
||||
toggleTagTeamRoll: this.toggleTagTeamRoll,
|
||||
toggleSelectedEffect: this.toggleSelectedEffect,
|
||||
submitRoll: this.submitRoll
|
||||
},
|
||||
|
|
@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
context.reactionOverride = this.reactionOverride;
|
||||
}
|
||||
|
||||
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
|
||||
context.activeTagTeamRoll = true;
|
||||
context.tagTeamSelected = this.config.tagTeamSelected;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -215,11 +208,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
}
|
||||
}
|
||||
|
||||
static toggleTagTeamRoll() {
|
||||
this.config.tagTeamSelected = !this.config.tagTeamSelected;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static toggleSelectedEffect(_event, button) {
|
||||
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
|
||||
this.render();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
|
||||
import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
this.reject = reject;
|
||||
this.actor = actor;
|
||||
this.damage = damage;
|
||||
|
||||
this.damageType = damageType;
|
||||
this.rulesDefault = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
|
|
@ -20,14 +21,20 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
this.rulesDefault
|
||||
);
|
||||
|
||||
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
|
||||
const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
|
||||
const maxArmorMarks = canApplyArmor ? availableArmor : 0;
|
||||
|
||||
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
|
||||
acc[foundry.utils.randomID()] = { selected: false };
|
||||
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
|
||||
const armor = orderedArmorSources.reduce((acc, { document }) => {
|
||||
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
|
||||
acc.push({
|
||||
effect: document,
|
||||
marks: [...Array(max).keys()].reduce((acc, _, index) => {
|
||||
const spent = index < current;
|
||||
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
|
||||
return acc;
|
||||
}, {});
|
||||
}, {})
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
|
||||
(acc, _) => {
|
||||
acc[foundry.utils.randomID()] = { selected: false };
|
||||
|
|
@ -121,13 +128,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
context.thresholdImmunities =
|
||||
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
|
||||
|
||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
|
||||
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
|
||||
this.getDamageInfo();
|
||||
|
||||
context.armorScore = this.actor.system.armorScore;
|
||||
context.armorScore = this.actor.system.armorScore.max;
|
||||
context.armorMarks = currentMarks;
|
||||
context.basicMarksUsed =
|
||||
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
|
||||
const stressReductionStress = this.availableStressReductions
|
||||
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
|
||||
|
|
@ -141,16 +146,30 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
: null;
|
||||
|
||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
context.marks = {
|
||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
||||
const mark = this.marks.armor[key];
|
||||
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
|
||||
context.maxArmorUsed = maxArmorUsed;
|
||||
context.availableArmor = availableArmor;
|
||||
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
const armorSources = [];
|
||||
for (const source of this.marks.armor) {
|
||||
const parent = source.effect.origin
|
||||
? await foundry.utils.fromUuid(source.effect.origin)
|
||||
: source.effect.parent;
|
||||
|
||||
const useEffectName = parent.type === 'armor' || parent instanceof Actor;
|
||||
const label = useEffectName ? source.effect.name : parent.name;
|
||||
armorSources.push({
|
||||
label: label,
|
||||
uuid: source.effect.uuid,
|
||||
marks: source.marks
|
||||
});
|
||||
}
|
||||
context.marks = {
|
||||
armor: armorSources,
|
||||
stress: this.marks.stress
|
||||
};
|
||||
|
||||
context.usesStressArmor = Object.keys(context.marks.stress).length;
|
||||
context.availableStressReductions = this.availableStressReductions;
|
||||
|
||||
context.damage = getDamageLabel(this.damage);
|
||||
|
|
@ -167,27 +186,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
getDamageInfo = () => {
|
||||
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
|
||||
const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected));
|
||||
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
|
||||
const stressReductions = this.availableStressReductions
|
||||
? Object.values(this.availableStressReductions).filter(red => red.selected)
|
||||
: [];
|
||||
const currentMarks =
|
||||
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
|
||||
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length;
|
||||
|
||||
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
|
||||
const availableArmor =
|
||||
maxArmorUsed -
|
||||
this.marks.armor.reduce((acc, source) => {
|
||||
acc += Object.values(source.marks).filter(x => x.selected).length;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
const armorMarkReduction =
|
||||
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
|
||||
let currentDamage = Math.max(
|
||||
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
|
||||
0
|
||||
);
|
||||
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
|
||||
if (this.reduceSeverity) {
|
||||
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
|
||||
}
|
||||
|
||||
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
|
||||
|
||||
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
|
||||
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
|
||||
};
|
||||
|
||||
static toggleRules() {
|
||||
|
|
@ -195,13 +218,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
|
||||
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
|
||||
this.marks = {
|
||||
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
|
||||
const mark = this.marks.armor[key];
|
||||
armor: this.marks.armor.map((mark, index) => {
|
||||
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
|
||||
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
return { ...mark, selected: keepSelectValue ? mark.selected : false };
|
||||
}),
|
||||
stress: this.marks.stress
|
||||
};
|
||||
|
||||
|
|
@ -209,8 +229,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
static setMarks(_, target) {
|
||||
const currentMark = this.marks[target.dataset.type][target.dataset.key];
|
||||
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
|
||||
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
|
||||
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
|
||||
|
||||
if (!currentMark.selected && currentDamage === 0) {
|
||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
|
||||
|
|
@ -218,12 +238,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
if (this.rulesOn) {
|
||||
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
|
||||
if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) {
|
||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const stressUsed = selectedStressMarks.length;
|
||||
if (target.dataset.type === 'armor' && stressUsed) {
|
||||
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
|
||||
if (updateResult === false) return;
|
||||
}
|
||||
|
||||
if (currentMark.selected) {
|
||||
const currentDamageLabel = getDamageLabel(currentDamage);
|
||||
for (let reduction of stressReductions) {
|
||||
|
|
@ -232,8 +258,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
}
|
||||
|
||||
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
|
||||
selectedStressMarks.forEach(mark => (mark.selected = false));
|
||||
if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
|
||||
for (const source of this.marks.armor) {
|
||||
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
|
||||
if (match) {
|
||||
source.marks[match].selected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentMark.armorMarkId = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +275,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
this.render();
|
||||
}
|
||||
|
||||
updateStressArmor(armorMarkId, select) {
|
||||
let stressMarkKey = null;
|
||||
if (select) {
|
||||
stressMarkKey = Object.keys(this.marks.stress).find(
|
||||
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
|
||||
);
|
||||
} else {
|
||||
stressMarkKey = Object.keys(this.marks.stress).find(
|
||||
key => this.marks.stress[key].armorMarkId === armorMarkId
|
||||
);
|
||||
if (!stressMarkKey)
|
||||
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
|
||||
}
|
||||
|
||||
if (!stressMarkKey) return false;
|
||||
|
||||
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
|
||||
}
|
||||
|
||||
static useStressReduction(_, target) {
|
||||
const damageValue = Number(target.dataset.reduction);
|
||||
const stressReduction = this.availableStressReductions[damageValue];
|
||||
|
|
@ -279,11 +332,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
|
|||
}
|
||||
|
||||
static async takeDamage() {
|
||||
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
||||
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
|
||||
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
|
||||
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
|
||||
const armorChanges = this.marks.armor.reduce((acc, source) => {
|
||||
const amount = Object.values(source.marks).filter(x => x.selected).length;
|
||||
if (amount) acc.push({ uuid: source.effect.uuid, amount });
|
||||
|
||||
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
|
||||
return acc;
|
||||
}, []);
|
||||
const stressSpent =
|
||||
selectedStressMarks.filter(x => x.armorMarkId).length +
|
||||
stressReductions.reduce((acc, red) => acc + red.cost, 0);
|
||||
|
||||
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
|
||||
await this.close(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
|
|||
const msg = {
|
||||
user: game.user.id,
|
||||
system: {
|
||||
moves: moves,
|
||||
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })),
|
||||
actor: this.actor.uuid
|
||||
},
|
||||
speaker: cls.getSpeaker(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
|
@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
|
|||
return acc;
|
||||
}, {})
|
||||
};
|
||||
|
||||
await this.message.update(update);
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
|
||||
await this.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { MemberData } from '../../data/tagTeamData.mjs';
|
||||
import { getCritDamageBonus } from '../../helpers/utils.mjs';
|
||||
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
|
||||
import Party from '../sheets/actors/party.mjs';
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
|
||||
|
|
@ -7,15 +9,23 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
constructor(party) {
|
||||
super();
|
||||
|
||||
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
this.party = party;
|
||||
this.partyMembers = party.system.partyMembers
|
||||
.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
|
||||
.map(member => ({
|
||||
...member.toObject(),
|
||||
uuid: member.uuid,
|
||||
id: member.id,
|
||||
selected: false
|
||||
}));
|
||||
this.intiator = null;
|
||||
this.openForAllPlayers = true;
|
||||
|
||||
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
|
||||
if (refreshType === RefreshType.TagTeamRoll) {
|
||||
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
this.tabGroups.application = Object.keys(party.system.tagTeam.members).length
|
||||
? 'tagTeamRoll'
|
||||
: 'initialization';
|
||||
|
||||
Hooks.on(socketEvent.Refresh, this.tagTeamRefresh.bind());
|
||||
}
|
||||
|
||||
get title() {
|
||||
|
|
@ -24,324 +34,639 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
|
|||
|
||||
static DEFAULT_OPTIONS = {
|
||||
tag: 'form',
|
||||
id: 'TagTeamDialog',
|
||||
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
|
||||
position: { width: 550, height: 'auto' },
|
||||
actions: {
|
||||
removeMember: TagTeamDialog.#removeMember,
|
||||
unlinkMessage: TagTeamDialog.#unlinkMessage,
|
||||
selectMessage: TagTeamDialog.#selectMessage,
|
||||
createTagTeam: TagTeamDialog.#createTagTeam
|
||||
toggleSelectMember: TagTeamDialog.#toggleSelectMember,
|
||||
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll,
|
||||
makeRoll: TagTeamDialog.#makeRoll,
|
||||
removeRoll: TagTeamDialog.#removeRoll,
|
||||
rerollDice: TagTeamDialog.#rerollDice,
|
||||
makeDamageRoll: TagTeamDialog.#makeDamageRoll,
|
||||
removeDamageRoll: TagTeamDialog.#removeDamageRoll,
|
||||
rerollDamageDice: TagTeamDialog.#rerollDamageDice,
|
||||
selectRoll: TagTeamDialog.#selectRoll,
|
||||
cancelRoll: TagTeamDialog.#onCancelRoll,
|
||||
finishRoll: TagTeamDialog.#finishRoll
|
||||
},
|
||||
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
application: {
|
||||
id: 'tag-team-dialog',
|
||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
|
||||
initialization: {
|
||||
id: 'initialization',
|
||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs'
|
||||
},
|
||||
tagTeamRoll: {
|
||||
id: 'tagTeamRoll',
|
||||
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
/** @inheritdoc */
|
||||
static TABS = {
|
||||
application: {
|
||||
tabs: [{ id: 'initialization' }, { id: 'tagTeamRoll' }]
|
||||
}
|
||||
};
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
|
||||
for (const element of htmlElement.querySelectorAll('.roll-type-select'))
|
||||
element.addEventListener('change', this.updateRollType.bind(this));
|
||||
}
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options);
|
||||
context.hopeCost = this.hopeCost;
|
||||
context.data = this.data;
|
||||
|
||||
context.memberOptions = this.party.filter(c => !this.data.members[c.id]);
|
||||
context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]);
|
||||
|
||||
context.members = Object.keys(this.data.members).map(id => {
|
||||
const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null;
|
||||
|
||||
context.usesDamage =
|
||||
context.usesDamage === undefined
|
||||
? roll?.system.hasDamage
|
||||
: context.usesDamage && roll?.system.hasDamage;
|
||||
return {
|
||||
character: this.party.find(x => x.id === id),
|
||||
selected: this.data.members[id].selected,
|
||||
roll: roll,
|
||||
damageValues: roll
|
||||
? Object.keys(roll.system.damage).map(key => ({
|
||||
key: key,
|
||||
name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label),
|
||||
total: roll.system.damage[key].total
|
||||
}))
|
||||
: null
|
||||
};
|
||||
});
|
||||
|
||||
const initiatorChar = this.party.find(x => x.id === this.data.initiator.id);
|
||||
context.initiator = {
|
||||
character: initiatorChar,
|
||||
cost: this.data.initiator.cost
|
||||
};
|
||||
|
||||
const selectedMember = Object.values(context.members).find(x => x.selected && x.roll);
|
||||
const selectedIsCritical = selectedMember?.roll?.system?.isCritical;
|
||||
context.selectedData = {
|
||||
result: selectedMember
|
||||
? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}`
|
||||
: null,
|
||||
damageValues: null
|
||||
};
|
||||
|
||||
for (const member of Object.values(context.members)) {
|
||||
if (!member.roll) continue;
|
||||
if (context.usesDamage) {
|
||||
if (!context.selectedData.damageValues) context.selectedData.damageValues = {};
|
||||
for (let damage of member.damageValues) {
|
||||
const damageTotal = member.roll.system.isCritical
|
||||
? damage.total
|
||||
: selectedIsCritical
|
||||
? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula))
|
||||
: damage.total;
|
||||
if (context.selectedData.damageValues[damage.key]) {
|
||||
context.selectedData.damageValues[damage.key].total += damageTotal;
|
||||
} else {
|
||||
context.selectedData.damageValues[damage.key] = {
|
||||
...foundry.utils.deepClone(damage),
|
||||
total: damageTotal
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.showResult = Object.values(context.members).reduce((enabled, member) => {
|
||||
if (!member.roll) return enabled;
|
||||
if (context.usesDamage) {
|
||||
enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0;
|
||||
} else {
|
||||
enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll);
|
||||
}
|
||||
|
||||
return enabled;
|
||||
}, null);
|
||||
|
||||
context.createDisabled =
|
||||
!context.selectedData.result ||
|
||||
!this.data.initiator.id ||
|
||||
Object.keys(this.data.members).length === 0 ||
|
||||
Object.values(context.members).some(x =>
|
||||
context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll
|
||||
);
|
||||
context.isEditable = this.getIsEditable();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
async updateSource(update) {
|
||||
await this.data.updateSource(update);
|
||||
async _preparePartContext(partId, context, options) {
|
||||
const partContext = await super._preparePartContext(partId, context, options);
|
||||
switch (partId) {
|
||||
case 'initialization':
|
||||
partContext.tagTeamFields = this.party.system.schema.fields.tagTeam.fields;
|
||||
partContext.memberSelection = this.partyMembers;
|
||||
const selectedMembers = partContext.memberSelection.filter(x => x.selected);
|
||||
|
||||
if (game.user.isGM) {
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
partContext.allSelected = selectedMembers.length === 2;
|
||||
partContext.canStartTagTeam = partContext.allSelected && this.initiator;
|
||||
partContext.initiator = this.initiator;
|
||||
partContext.initiatorOptions = selectedMembers.map(x => ({ value: x.id, label: x.name }));
|
||||
partContext.initiatorDisabled = !selectedMembers.length;
|
||||
partContext.openForAllPlayers = this.openForAllPlayers;
|
||||
|
||||
break;
|
||||
case 'tagTeamRoll':
|
||||
partContext.fields = this.party.system.schema.fields.tagTeam.fields;
|
||||
partContext.data = this.party.system.tagTeam;
|
||||
partContext.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
|
||||
partContext.traitOptions = CONFIG.DH.ACTOR.abilities;
|
||||
|
||||
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
|
||||
const critSelected = !selectedRoll
|
||||
? undefined
|
||||
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
|
||||
|
||||
partContext.members = {};
|
||||
for (const actorId in this.party.system.tagTeam.members) {
|
||||
const data = this.party.system.tagTeam.members[actorId];
|
||||
const actor = game.actors.get(actorId);
|
||||
|
||||
const rollOptions = [];
|
||||
const damageRollOptions = [];
|
||||
for (const item of actor.items) {
|
||||
if (item.system.metadata.hasActions) {
|
||||
const actions = [
|
||||
...item.system.actions,
|
||||
...(item.system.attack ? [item.system.attack] : [])
|
||||
];
|
||||
for (const action of actions) {
|
||||
if (action.hasRoll) {
|
||||
const actionItem = {
|
||||
value: action.uuid,
|
||||
label: action.name,
|
||||
group: item.name,
|
||||
baseAction: action.baseAction
|
||||
};
|
||||
|
||||
if (action.hasDamage) damageRollOptions.push(actionItem);
|
||||
else rollOptions.push(actionItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const damage = data.rollData?.options?.damage;
|
||||
partContext.hasDamage |= Boolean(damage);
|
||||
const critHitPointsDamage = await this.getCriticalDamage(damage);
|
||||
|
||||
partContext.members[actorId] = {
|
||||
...data,
|
||||
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
|
||||
key: actorId,
|
||||
readyToRoll: Boolean(data.rollChoice),
|
||||
hasRolled: Boolean(data.rollData),
|
||||
rollOptions,
|
||||
damageRollOptions,
|
||||
damage: damage,
|
||||
critDamage: critHitPointsDamage,
|
||||
useCritDamage:
|
||||
critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
|
||||
};
|
||||
}
|
||||
|
||||
partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members);
|
||||
partContext.joinedRoll = await this.getJoinedRoll({
|
||||
overrideIsCritical: critSelected,
|
||||
displayVersion: true
|
||||
});
|
||||
} else {
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.GMUpdate,
|
||||
data: {
|
||||
action: GMUpdateEvent.UpdateSetting,
|
||||
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
|
||||
update: this.data.toObject(),
|
||||
refresh: { refreshType: RefreshType.TagTeamRoll }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
static async updateData(_event, _element, formData) {
|
||||
const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object);
|
||||
const update = { initiator: initiator };
|
||||
if (selectedAddMember) {
|
||||
const member = await foundry.utils.fromUuid(selectedAddMember);
|
||||
update[`members.${member.id}`] = { messageId: null };
|
||||
return partContext;
|
||||
}
|
||||
|
||||
await this.updateSource(update);
|
||||
static async updateData(_event, _, formData) {
|
||||
const { initiator, openForAllPlayers, ...partyData } = foundry.utils.expandObject(formData.object);
|
||||
this.initiator = initiator;
|
||||
this.openForAllPlayers = openForAllPlayers !== undefined ? openForAllPlayers : this.openForAllPlayers;
|
||||
|
||||
this.updatePartyData(partyData);
|
||||
}
|
||||
|
||||
async updatePartyData(update, options = { render: true }) {
|
||||
const gmUpdate = async update => {
|
||||
await this.party.update(update);
|
||||
this.render();
|
||||
game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' }
|
||||
});
|
||||
};
|
||||
|
||||
await emitAsGM(
|
||||
GMUpdateEvent.UpdateDocument,
|
||||
gmUpdate,
|
||||
update,
|
||||
this.party.uuid,
|
||||
options.render ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } : undefined
|
||||
);
|
||||
}
|
||||
|
||||
static async #removeMember(_, button) {
|
||||
const update = { [`members.${button.dataset.characterId}`]: _del };
|
||||
if (this.data.initiator.id === button.dataset.characterId) {
|
||||
update.iniator = { id: null };
|
||||
}
|
||||
|
||||
await this.updateSource(update);
|
||||
}
|
||||
|
||||
static async #unlinkMessage(_, button) {
|
||||
await this.updateSource({ [`members.${button.id}.messageId`]: null });
|
||||
}
|
||||
|
||||
static async #selectMessage(_, button) {
|
||||
const member = this.data.members[button.id];
|
||||
const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected);
|
||||
const curretSelectedUpdate =
|
||||
currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {};
|
||||
await this.updateSource({
|
||||
members: {
|
||||
[`${button.id}`]: { selected: !member.selected },
|
||||
...curretSelectedUpdate
|
||||
}
|
||||
getIsEditable() {
|
||||
return this.party.system.partyMembers.some(actor => {
|
||||
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
|
||||
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
|
||||
});
|
||||
}
|
||||
|
||||
static async #createTagTeam() {
|
||||
const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
|
||||
const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
|
||||
tagTeamRefresh = ({ refreshType, action }) => {
|
||||
if (refreshType !== RefreshType.TagTeamRoll) return;
|
||||
|
||||
if (this.data.initiator.cost) {
|
||||
const initiator = this.party.find(x => x.id === this.data.initiator.id);
|
||||
if (initiator.system.resources.hope.value < this.data.initiator.cost) {
|
||||
switch (action) {
|
||||
case 'startTagTeamRoll':
|
||||
this.tabGroups.application = 'tagTeamRoll';
|
||||
break;
|
||||
case 'refresh':
|
||||
this.render();
|
||||
break;
|
||||
case 'close':
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
async close(options = {}) {
|
||||
/* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
|
||||
if (options.closeKey) return;
|
||||
|
||||
Hooks.off(socketEvent.Refresh, this.tagTeamRefresh);
|
||||
return super.close(options);
|
||||
}
|
||||
|
||||
checkInitiatorHopeError(initiator) {
|
||||
if (initiator.cost && initiator.memberId) {
|
||||
const actor = game.actors.get(initiator.memberId);
|
||||
if (actor.system.resources.hope.value < initiator.cost) {
|
||||
return ui.notifications.warn(
|
||||
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const secondaryRolls = Object.keys(this.data.members)
|
||||
.filter(key => key !== mainRollId)
|
||||
.map(key => game.messages.get(this.data.members[key].messageId));
|
||||
//#region Initialization
|
||||
static #toggleSelectMember(_, button) {
|
||||
const member = this.partyMembers.find(x => x.id === button.dataset.id);
|
||||
if (member.selected && this.initiator?.memberId === member.id) this.initiator = null;
|
||||
|
||||
const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
|
||||
const criticalRoll = systemData.roll.isCritical;
|
||||
for (let roll of secondaryRolls) {
|
||||
if (roll.system.hasDamage) {
|
||||
for (let key in roll.system.damage) {
|
||||
var damage = roll.system.damage[key];
|
||||
const damageTotal =
|
||||
!roll.system.isCritical && criticalRoll
|
||||
? (await getCritDamageBonus(damage.formula)) + damage.total
|
||||
: damage.total;
|
||||
const updatedDamageParts = damage.parts;
|
||||
if (systemData.damage[key]) {
|
||||
if (!roll.system.isCritical && criticalRoll) {
|
||||
for (let part of updatedDamageParts) {
|
||||
member.selected = !member.selected;
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async #startTagTeamRoll() {
|
||||
const error = this.checkInitiatorHopeError(this.initiator);
|
||||
if (error) return error;
|
||||
|
||||
await this.party.update({
|
||||
'system.tagTeam': _replace(
|
||||
new game.system.api.data.TagTeamData({
|
||||
...this.party.system.tagTeam.toObject(),
|
||||
initiator: this.initiator,
|
||||
members: this.partyMembers.reduce((acc, member) => {
|
||||
if (member.selected)
|
||||
acc[member.id] = {
|
||||
name: member.name,
|
||||
img: member.img,
|
||||
rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id
|
||||
};
|
||||
return acc;
|
||||
}, {})
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id };
|
||||
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, hookData);
|
||||
game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.TagTeamStart,
|
||||
data: hookData
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
//#endregion
|
||||
//#region Tag Team Roll
|
||||
|
||||
async getInfoTexts(members) {
|
||||
let rollsAreFinished = true;
|
||||
let rollIsSelected = false;
|
||||
for (const member of Object.values(members)) {
|
||||
const rollFinished = Boolean(member.rollData);
|
||||
const damageFinished =
|
||||
member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true;
|
||||
|
||||
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
|
||||
rollIsSelected = rollIsSelected || member.selected;
|
||||
}
|
||||
|
||||
let hint = null;
|
||||
if (!rollsAreFinished) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.completeRolls');
|
||||
else if (!rollIsSelected) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.selectRoll');
|
||||
|
||||
return hint;
|
||||
}
|
||||
|
||||
async updateRollType(event) {
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${event.target.dataset.member}`]: {
|
||||
rollType: event.target.value,
|
||||
rollChoice: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #removeRoll(_, button) {
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${button.dataset.member}`]: {
|
||||
rollData: null,
|
||||
rollChoice: null,
|
||||
selected: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #makeRoll(event, button) {
|
||||
const { member } = button.dataset;
|
||||
|
||||
let result = null;
|
||||
switch (this.party.system.tagTeam.members[member].rollType) {
|
||||
case CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id:
|
||||
result = await this.makeTraitRoll(member);
|
||||
break;
|
||||
case CONFIG.DH.GENERAL.tagTeamRollTypes.ability.id:
|
||||
case CONFIG.DH.GENERAL.tagTeamRollTypes.damageAbility.id:
|
||||
result = await this.makeAbilityRoll(event, member);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||
|
||||
const rollData = result.messageRoll.toJSON();
|
||||
delete rollData.options.messageRoll;
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${member}.rollData`]: rollData
|
||||
});
|
||||
}
|
||||
|
||||
async makeTraitRoll(memberKey) {
|
||||
const actor = game.actors.find(x => x.id === memberKey);
|
||||
if (!actor) return;
|
||||
|
||||
const memberData = this.party.system.tagTeam.members[memberKey];
|
||||
return await actor.rollTrait(memberData.rollChoice, {
|
||||
skips: {
|
||||
createMessage: true,
|
||||
resources: true,
|
||||
triggers: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async makeAbilityRoll(event, memberKey) {
|
||||
const actor = game.actors.find(x => x.id === memberKey);
|
||||
if (!actor) return;
|
||||
|
||||
const memberData = this.party.system.tagTeam.members[memberKey];
|
||||
const action = await foundry.utils.fromUuid(memberData.rollChoice);
|
||||
|
||||
return await action.use(event, {
|
||||
skips: {
|
||||
createMessage: true,
|
||||
resources: true,
|
||||
triggers: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #rerollDice(_, button) {
|
||||
const { member, diceType } = button.dataset;
|
||||
const memberData = this.party.system.tagTeam.members[member];
|
||||
|
||||
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4;
|
||||
|
||||
const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll(
|
||||
memberData.rollData,
|
||||
dieIndex,
|
||||
diceType
|
||||
);
|
||||
const rollData = parsedRoll.toJSON();
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${member}.rollData`]: {
|
||||
...rollData,
|
||||
options: {
|
||||
...rollData.options,
|
||||
roll: newRoll
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #makeDamageRoll(event, button) {
|
||||
const { memberKey } = button.dataset;
|
||||
const actor = game.actors.find(x => x.id === memberKey);
|
||||
if (!actor) return;
|
||||
|
||||
const memberData = this.party.system.tagTeam.members[memberKey];
|
||||
const action = await foundry.utils.fromUuid(memberData.rollChoice);
|
||||
const config = {
|
||||
...memberData.rollData.options,
|
||||
dialog: {
|
||||
configure: !event.shiftKey
|
||||
},
|
||||
skips: {
|
||||
createMessage: true,
|
||||
resources: true,
|
||||
triggers: true
|
||||
}
|
||||
};
|
||||
|
||||
await action.workflow.get('damage').execute(config, null, true);
|
||||
if (!config.damage) return;
|
||||
|
||||
const current = this.party.system.tagTeam.members[memberKey].rollData;
|
||||
await this.updatePartyData({
|
||||
[`system.tagTeam.members.${memberKey}.rollData`]: {
|
||||
...current,
|
||||
options: {
|
||||
...current.options,
|
||||
damage: config.damage
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #removeDamageRoll(_, button) {
|
||||
const { memberKey } = button.dataset;
|
||||
const current = this.party.system.tagTeam.members[memberKey].rollData;
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${memberKey}.rollData`]: {
|
||||
...current,
|
||||
options: {
|
||||
...current.options,
|
||||
damage: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async #rerollDamageDice(_, button) {
|
||||
const { memberKey, damageKey, part, dice } = button.dataset;
|
||||
const memberData = this.party.system.tagTeam.members[memberKey];
|
||||
const partData = memberData.rollData.options.damage[damageKey].parts[part];
|
||||
const activeDiceResultKey = Object.keys(partData.dice[dice].results).find(
|
||||
index => partData.dice[dice].results[index].active
|
||||
);
|
||||
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(
|
||||
partData,
|
||||
dice,
|
||||
activeDiceResultKey
|
||||
);
|
||||
|
||||
const rollData = this.party.system.tagTeam.members[memberKey].rollData;
|
||||
rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => {
|
||||
if (index !== Number.parseInt(part)) return damagePart;
|
||||
|
||||
return {
|
||||
...damagePart,
|
||||
total: parsedRoll.total,
|
||||
dice: rerolledDice
|
||||
};
|
||||
});
|
||||
rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => {
|
||||
acc += part.total;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members.${memberKey}.rollData`]: rollData
|
||||
});
|
||||
}
|
||||
|
||||
async getCriticalDamage(damage) {
|
||||
const newDamage = foundry.utils.deepClone(damage);
|
||||
for (let key in newDamage) {
|
||||
var damage = newDamage[key];
|
||||
damage.formula = '';
|
||||
damage.total = 0;
|
||||
|
||||
for (let part of damage.parts) {
|
||||
const criticalDamage = await getCritDamageBonus(part.formula);
|
||||
if (criticalDamage) {
|
||||
damage.formula = `${damage.formula} + ${criticalDamage}`;
|
||||
part.formula = `${part.formula} + ${criticalDamage}`;
|
||||
part.modifierTotal = part.modifierTotal + criticalDamage;
|
||||
part.modifierTotal += criticalDamage;
|
||||
part.total += criticalDamage;
|
||||
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
|
||||
part.roll = new Roll(part.formula);
|
||||
}
|
||||
|
||||
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
|
||||
damage.total += part.total;
|
||||
}
|
||||
}
|
||||
|
||||
systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`;
|
||||
systemData.damage[key].total += damageTotal;
|
||||
systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts];
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
async getNonCriticalDamage(config) {
|
||||
const newDamage = foundry.utils.deepClone(config.damage);
|
||||
for (let key in newDamage) {
|
||||
var damage = newDamage[key];
|
||||
damage.formula = '';
|
||||
damage.total = 0;
|
||||
|
||||
for (let part of damage.parts) {
|
||||
const critDamageBonus = await getCritDamageBonus(part.formula);
|
||||
part.modifierTotal -= critDamageBonus;
|
||||
part.total -= critDamageBonus;
|
||||
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
|
||||
part.roll = new Roll(part.formula);
|
||||
|
||||
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
|
||||
damage.total += part.total;
|
||||
}
|
||||
}
|
||||
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
static async #selectRoll(_, button) {
|
||||
const { memberKey } = button.dataset;
|
||||
this.updatePartyData({
|
||||
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
|
||||
(acc, [key, member]) => {
|
||||
acc[key] = { selected: key === memberKey ? !member.selected : false };
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
|
||||
const memberValues = Object.values(this.party.system.tagTeam.members);
|
||||
const selectedRoll = memberValues.find(x => x.selected);
|
||||
let baseMainRoll = selectedRoll ?? memberValues[0];
|
||||
let baseSecondaryRoll = selectedRoll
|
||||
? memberValues.find(x => !x.selected)
|
||||
: memberValues.length > 1
|
||||
? memberValues[1]
|
||||
: null;
|
||||
|
||||
if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
|
||||
|
||||
const mainRoll = new MemberData(baseMainRoll.toObject());
|
||||
const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
|
||||
const systemData = mainRoll.rollData.options;
|
||||
const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
|
||||
if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
|
||||
|
||||
if (secondaryRollData?.options.hasDamage) {
|
||||
const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
|
||||
? await this.getCriticalDamage(secondaryRollData.options.damage)
|
||||
: secondaryRollData.options.damage;
|
||||
if (systemData.damage) {
|
||||
for (const key in secondaryDamage) {
|
||||
const damage = secondaryDamage[key];
|
||||
systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula]
|
||||
.filter(x => x)
|
||||
.join(' + ');
|
||||
systemData.damage[key].total += damage.total;
|
||||
systemData.damage[key].parts.push(...damage.parts);
|
||||
}
|
||||
} else {
|
||||
systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts };
|
||||
}
|
||||
}
|
||||
systemData.damage = secondaryDamage;
|
||||
}
|
||||
}
|
||||
|
||||
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
|
||||
return mainRoll;
|
||||
}
|
||||
|
||||
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
|
||||
this.cancelRoll(options);
|
||||
}
|
||||
|
||||
async cancelRoll(options = { confirm: true }) {
|
||||
if (options.confirm) {
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: {
|
||||
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmTitle')
|
||||
},
|
||||
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmText')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
await this.updatePartyData(
|
||||
{
|
||||
'system.tagTeam': {
|
||||
initiator: null,
|
||||
members: _replace({})
|
||||
}
|
||||
},
|
||||
{ render: false }
|
||||
);
|
||||
|
||||
this.close();
|
||||
game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: { refreshType: RefreshType.TagTeamRoll, action: 'close' }
|
||||
});
|
||||
}
|
||||
|
||||
static async #finishRoll() {
|
||||
const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator);
|
||||
if (error) return error;
|
||||
|
||||
const mainRoll = (await this.getJoinedRoll()).rollData;
|
||||
|
||||
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
|
||||
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
|
||||
const cls = getDocumentClass('ChatMessage'),
|
||||
msgData = {
|
||||
type: 'dualityRoll',
|
||||
user: game.user.id,
|
||||
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
|
||||
speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
|
||||
system: systemData,
|
||||
rolls: mainRoll.rolls,
|
||||
speaker: cls.getSpeaker({ actor: mainActor }),
|
||||
system: mainRoll.options,
|
||||
rolls: [mainRoll],
|
||||
sound: null,
|
||||
flags: { core: { RollTable: true } }
|
||||
};
|
||||
|
||||
await cls.create(msgData);
|
||||
|
||||
/* Handle resource updates from the finished TagTeamRoll */
|
||||
const tagTeamData = this.party.system.tagTeam;
|
||||
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
|
||||
for (let memberId of Object.keys(this.data.members)) {
|
||||
for (let memberId in tagTeamData.members) {
|
||||
const resourceUpdates = [];
|
||||
const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1;
|
||||
if (memberId === this.data.initiator.id) {
|
||||
const value = this.data.initiator.cost
|
||||
const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1;
|
||||
if (memberId === tagTeamData.initiator.memberId) {
|
||||
const value = tagTeamData.initiator.cost
|
||||
? rollGivesHope
|
||||
? 1 - this.data.initiator.cost
|
||||
: -this.data.initiator.cost
|
||||
? 1 - tagTeamData.initiator.cost
|
||||
: -tagTeamData.initiator.cost
|
||||
: 1;
|
||||
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
|
||||
} else if (rollGivesHope) {
|
||||
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
|
||||
}
|
||||
if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
|
||||
if (systemData.roll.result.duality === -1) {
|
||||
if (mainRoll.options.roll.isCritical)
|
||||
resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
|
||||
if (mainRoll.options.roll.result.duality === -1) {
|
||||
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
|
||||
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
|
||||
}
|
||||
|
||||
this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
|
||||
game.actors.get(memberId).modifyResource(resourceUpdates);
|
||||
}
|
||||
|
||||
if (fearUpdate.value) {
|
||||
this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
|
||||
mainActor.modifyResource([fearUpdate]);
|
||||
}
|
||||
|
||||
/* Improve by fetching default from schema */
|
||||
const update = { members: [], initiator: { id: null, cost: 3 } };
|
||||
if (game.user.isGM) {
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update);
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.GMUpdate,
|
||||
data: {
|
||||
action: GMUpdateEvent.UpdateSetting,
|
||||
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
|
||||
update: update,
|
||||
refresh: { refreshType: RefreshType.TagTeamRoll }
|
||||
}
|
||||
});
|
||||
}
|
||||
/* Fin */
|
||||
this.cancelRoll({ confirm: false });
|
||||
}
|
||||
|
||||
static async assignRoll(char, message) {
|
||||
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
const character = settings.members[char.id];
|
||||
if (!character) return;
|
||||
|
||||
await settings.updateSource({ [`members.${char.id}.messageId`]: message.id });
|
||||
|
||||
if (game.user.isGM) {
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings);
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.GMUpdate,
|
||||
data: {
|
||||
action: GMUpdateEvent.UpdateSetting,
|
||||
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
|
||||
update: settings,
|
||||
refresh: { refreshType: RefreshType.TagTeamRoll }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async close(options = {}) {
|
||||
Hooks.off(socketEvent.Refresh, this.setupHooks);
|
||||
await super.close(options);
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs';
|
|||
export { default as CharacterSettings } from './character-settings.mjs';
|
||||
export { default as AdversarySettings } from './adversary-settings.mjs';
|
||||
export { default as CompanionSettings } from './companion-settings.mjs';
|
||||
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
|
||||
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
||||
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
||||
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ export default class DHActionConfig extends DHActionBaseConfig {
|
|||
const effectData = this._addEffectData.bind(this)();
|
||||
const data = this.action.toObject();
|
||||
|
||||
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
|
||||
const created = await game.system.api.documents.DhActiveEffect.createDialog(effectData, {
|
||||
parent: this.action.item,
|
||||
render: false
|
||||
});
|
||||
if (!created) return;
|
||||
|
||||
data.effects.push({ _id: created._id });
|
||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||
this.action.item.effects.get(created._id).sheet.render(true);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
|
|||
|
||||
static async editEffect(event) {
|
||||
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
|
||||
const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
|
||||
const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(
|
||||
this.getEffectDetails(id)
|
||||
);
|
||||
if (!updatedEffect) return;
|
||||
|
|
|
|||
|
|
@ -150,6 +150,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
|||
minLength: 0
|
||||
});
|
||||
});
|
||||
|
||||
htmlElement
|
||||
.querySelector('.armor-change-checkbox')
|
||||
?.addEventListener('change', this.armorChangeToggle.bind(this));
|
||||
|
||||
htmlElement
|
||||
.querySelector('.armor-damage-thresholds-checkbox')
|
||||
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
|
||||
}
|
||||
|
||||
async _prepareContext(options) {
|
||||
|
|
@ -186,21 +194,66 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
|||
}));
|
||||
break;
|
||||
case 'changes':
|
||||
const fields = this.document.system.schema.fields.changes.element.fields;
|
||||
partContext.changes = await Promise.all(
|
||||
foundry.utils
|
||||
.deepClone(context.source.changes)
|
||||
.map((c, i) => this._prepareChangeContext(c, i, fields))
|
||||
);
|
||||
const singleTypes = ['armor'];
|
||||
const typedChanges = context.source.changes.reduce((acc, change, index) => {
|
||||
if (singleTypes.includes(change.type)) {
|
||||
acc[change.type] = { ...change, index };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
partContext.changes = partContext.changes.filter(c => !!c);
|
||||
partContext.typedChanges = typedChanges;
|
||||
break;
|
||||
}
|
||||
|
||||
return partContext;
|
||||
}
|
||||
|
||||
_prepareChangeContext(change, index, fields) {
|
||||
armorChangeToggle(event) {
|
||||
if (event.target.checked) {
|
||||
this.addArmorChange();
|
||||
} else {
|
||||
this.removeTypedChange(event.target.dataset.index);
|
||||
}
|
||||
}
|
||||
|
||||
/* Could be generalised if needed later */
|
||||
addArmorChange() {
|
||||
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||
const changes = Object.values(submitData.system?.changes ?? {});
|
||||
changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue());
|
||||
return this.submit({ updateData: { system: { changes } } });
|
||||
}
|
||||
|
||||
removeTypedChange(indexString) {
|
||||
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||
const changes = Object.values(submitData.system.changes);
|
||||
const index = Number(indexString);
|
||||
changes.splice(index, 1);
|
||||
return this.submit({ updateData: { system: { changes } } });
|
||||
}
|
||||
|
||||
armorDamageThresholdToggle(event) {
|
||||
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
|
||||
const changes = Object.values(submitData.system?.changes ?? {});
|
||||
const index = Number(event.target.dataset.index);
|
||||
if (event.target.checked) {
|
||||
changes[index].value.damageThresholds = { major: 0, severe: 0 };
|
||||
} else {
|
||||
changes[index].value.damageThresholds = null;
|
||||
}
|
||||
|
||||
return this.submit({ updateData: { system: { changes } } });
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
_renderChange(context) {
|
||||
const { change, index, defaultPriority } = context;
|
||||
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
|
||||
|
||||
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
|
||||
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
|
||||
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
|
||||
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
|
||||
Object.assign(
|
||||
change,
|
||||
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
|
||||
|
|
@ -220,7 +273,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
|||
change,
|
||||
index,
|
||||
defaultPriority,
|
||||
fields
|
||||
fields,
|
||||
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
|
||||
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
|
||||
return r;
|
||||
}, {})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
@ -247,4 +304,34 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
|
|||
|
||||
return submitData;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
_processSubmitData(event, form, submitData, options) {
|
||||
if (this.options.isSetting) {
|
||||
// Settings should update source instead
|
||||
this.document.updateSource(submitData);
|
||||
this.render();
|
||||
} else {
|
||||
return super._processSubmitData(event, form, submitData, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an active effect config for a setting */
|
||||
static async configureSetting(effect, options = {}) {
|
||||
const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id });
|
||||
return new Promise(resolve => {
|
||||
const app = new this({ document, ...options, isSetting: true });
|
||||
app.addEventListener(
|
||||
'close',
|
||||
() => {
|
||||
const newEffect = app.document.toObject(true);
|
||||
newEffect.id = newEffect._id;
|
||||
delete newEffect._id;
|
||||
resolve(newEffect);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
app.render({ force: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
import autocomplete from 'autocompleter';
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
|
||||
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
constructor(effect) {
|
||||
super({});
|
||||
|
||||
this.effect = foundry.utils.deepClone(effect);
|
||||
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
|
||||
}
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
|
||||
tag: 'form',
|
||||
position: {
|
||||
width: 560
|
||||
},
|
||||
form: {
|
||||
submitOnChange: false,
|
||||
closeOnSubmit: false,
|
||||
handler: SettingActiveEffectConfig.#onSubmit
|
||||
},
|
||||
actions: {
|
||||
editImage: SettingActiveEffectConfig.#editImage,
|
||||
addChange: SettingActiveEffectConfig.#addChange,
|
||||
deleteChange: SettingActiveEffectConfig.#deleteChange
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
|
||||
tabs: { template: 'templates/generic/tab-navigation.hbs' },
|
||||
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
|
||||
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
|
||||
changes: {
|
||||
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
|
||||
scrollable: ['ol[data-changes]']
|
||||
},
|
||||
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
|
||||
};
|
||||
|
||||
static TABS = {
|
||||
sheet: {
|
||||
tabs: [
|
||||
{ id: 'details', icon: 'fa-solid fa-book' },
|
||||
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
|
||||
{ id: 'changes', icon: 'fa-solid fa-gears' }
|
||||
],
|
||||
initial: 'details',
|
||||
labelPrefix: 'EFFECT.TABS'
|
||||
}
|
||||
};
|
||||
|
||||
/**@inheritdoc */
|
||||
async _onFirstRender(context, options) {
|
||||
await super._onFirstRender(context, options);
|
||||
}
|
||||
|
||||
async _prepareContext(_options) {
|
||||
const context = await super._prepareContext(_options);
|
||||
context.source = this.effect;
|
||||
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
|
||||
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
_attachPartListeners(partId, htmlElement, options) {
|
||||
super._attachPartListeners(partId, htmlElement, options);
|
||||
const changeChoices = this.changeChoices;
|
||||
|
||||
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
|
||||
autocomplete({
|
||||
input: element,
|
||||
fetch: function (text, update) {
|
||||
if (!text) {
|
||||
update(changeChoices);
|
||||
} else {
|
||||
text = text.toLowerCase();
|
||||
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
|
||||
update(suggestions);
|
||||
}
|
||||
},
|
||||
render: function (item, search) {
|
||||
const label = game.i18n.localize(item.label);
|
||||
const matchIndex = label.toLowerCase().indexOf(search);
|
||||
|
||||
const beforeText = label.slice(0, matchIndex);
|
||||
const matchText = label.slice(matchIndex, matchIndex + search.length);
|
||||
const after = label.slice(matchIndex + search.length, label.length);
|
||||
|
||||
const element = document.createElement('li');
|
||||
element.innerHTML =
|
||||
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
|
||||
' ',
|
||||
' '
|
||||
);
|
||||
if (item.hint) {
|
||||
element.dataset.tooltip = game.i18n.localize(item.hint);
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
renderGroup: function (label) {
|
||||
const itemElement = document.createElement('div');
|
||||
itemElement.textContent = game.i18n.localize(label);
|
||||
return itemElement;
|
||||
},
|
||||
onSelect: function (item) {
|
||||
element.value = `system.${item.value}`;
|
||||
},
|
||||
click: e => e.fetch(),
|
||||
customize: function (_input, _inputRect, container) {
|
||||
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
|
||||
},
|
||||
minLength: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _preparePartContext(partId, context) {
|
||||
if (partId in context.tabs) context.tab = context.tabs[partId];
|
||||
switch (partId) {
|
||||
case 'details':
|
||||
context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) }));
|
||||
context.isActorEffect = false;
|
||||
context.isItemEffect = true;
|
||||
const useGeneric = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||
).showGenericStatusEffects;
|
||||
if (!useGeneric) {
|
||||
context.statuses = [
|
||||
...context.statuses,
|
||||
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
|
||||
value: status.id,
|
||||
label: game.i18n.localize(status.name)
|
||||
}))
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'changes':
|
||||
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
|
||||
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
|
||||
return modes;
|
||||
}, {});
|
||||
|
||||
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
|
||||
break;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
static async #onSubmit(_event, _form, formData) {
|
||||
this.data = foundry.utils.expandObject(formData.object);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a Document image.
|
||||
* @this {DocumentSheetV2}
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #editImage(_event, target) {
|
||||
if (target.nodeName !== 'IMG') {
|
||||
throw new Error('The editImage action is available only for IMG elements.');
|
||||
}
|
||||
|
||||
const attr = target.dataset.edit;
|
||||
const current = foundry.utils.getProperty(this.effect, attr);
|
||||
const fp = new FilePicker.implementation({
|
||||
current,
|
||||
type: 'image',
|
||||
callback: path => (target.src = path),
|
||||
position: {
|
||||
top: this.position.top + 40,
|
||||
left: this.position.left + 10
|
||||
}
|
||||
});
|
||||
|
||||
await fp.browse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new change to the effect's changes array.
|
||||
* @this {ActiveEffectConfig}
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #addChange() {
|
||||
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
||||
const updatedChanges = Object.values(changes ?? {});
|
||||
updatedChanges.push({});
|
||||
|
||||
this.effect = { ...rest, changes: updatedChanges };
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a change from the effect's changes array.
|
||||
* @this {ActiveEffectConfig}
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #deleteChange(event) {
|
||||
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
||||
const updatedChanges = Object.values(submitData.changes);
|
||||
const row = event.target.closest('li');
|
||||
const index = Number(row.dataset.index) || 0;
|
||||
updatedChanges.splice(index, 1);
|
||||
|
||||
this.effect = { ...submitData, changes: updatedChanges };
|
||||
this.render();
|
||||
}
|
||||
|
||||
static async configure(effect, options = {}) {
|
||||
return new Promise(resolve => {
|
||||
const app = new this(effect, options);
|
||||
app.addEventListener('close', () => resolve(app.data), { once: true });
|
||||
app.render({ force: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
|
|||
const effectIndex = this.move.effects.findIndex(x => x.id === id);
|
||||
const effect = this.move.effects[effectIndex];
|
||||
const updatedEffect =
|
||||
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
||||
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect);
|
||||
if (!updatedEffect) return;
|
||||
|
||||
await this.updateMove({
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
|
|||
changes.height = tokenSize;
|
||||
}
|
||||
|
||||
const deletions = { actorId: _del, actorLink: _del };
|
||||
const mergeOptions = { inplace: false, performDeletions: true };
|
||||
this._preview.updateSource(foundry.utils.mergeObject(changes, deletions, mergeOptions));
|
||||
// const deletions = { actorId: _del };
|
||||
// const mergeOptions = { inplace: false, performDeletions: true, actorLink: false };
|
||||
this._preview.updateSource(changes);
|
||||
|
||||
if (this._preview?.object?.destroyed === false) {
|
||||
this._preview.object.initializeSources();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import DHBaseActorSheet from '../api/base-actor.mjs';
|
||||
import DhDeathMove from '../../dialogs/deathMove.mjs';
|
||||
import { abilities } from '../../../config/actorConfig.mjs';
|
||||
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
||||
import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
|
||||
|
||||
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
|
||||
|
||||
|
|
@ -35,7 +34,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
cancelBeastform: CharacterSheet.#cancelBeastform,
|
||||
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
|
||||
useDowntime: this.useDowntime,
|
||||
viewParty: CharacterSheet.#viewParty
|
||||
viewParty: CharacterSheet.#viewParty,
|
||||
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
|
||||
},
|
||||
window: {
|
||||
resizable: true,
|
||||
|
|
@ -639,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
}
|
||||
|
||||
async updateArmorMarks(event) {
|
||||
const armor = this.document.system.armor;
|
||||
if (!armor) return;
|
||||
const inputValue = Number(event.currentTarget.value);
|
||||
const { value, max } = this.document.system.armorScore;
|
||||
const changeValue = Math.min(inputValue - value, max - value);
|
||||
|
||||
const maxMarks = this.document.system.armorScore;
|
||||
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
|
||||
await armor.update({ 'system.marks.value': value });
|
||||
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue;
|
||||
this.document.system.updateArmorValue({ value: changeValue });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -720,35 +720,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
* Rolls an attribute check based on the clicked button's dataset attribute.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #rollAttribute(event, button) {
|
||||
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
|
||||
const config = {
|
||||
event: event,
|
||||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
|
||||
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
|
||||
roll: {
|
||||
trait: button.dataset.attribute,
|
||||
type: 'trait'
|
||||
},
|
||||
hasRoll: true,
|
||||
actionType: 'action',
|
||||
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
|
||||
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
})
|
||||
};
|
||||
const result = await this.document.diceRoll(config);
|
||||
static async #rollAttribute(_event, button) {
|
||||
const result = await this.document.rollTrait(button.dataset.attribute);
|
||||
if (!result) return;
|
||||
|
||||
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
|
||||
const costResources =
|
||||
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
|
||||
{};
|
||||
config.resourceUpdates.addResources(costResources);
|
||||
await config.resourceUpdates.updateResources();
|
||||
result.resourceUpdates.addResources(costResources);
|
||||
await result.resourceUpdates.updateResources();
|
||||
}
|
||||
|
||||
//TODO: redo toggleEquipItem method
|
||||
|
|
@ -823,10 +804,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
* Toggles ArmorScore resource value.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #toggleArmor(_, button, element) {
|
||||
const ArmorValue = Number.parseInt(button.dataset.value);
|
||||
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
|
||||
await this.document.system.armor.update({ 'system.marks.value': newValue });
|
||||
static async #toggleArmor(_, button, _element) {
|
||||
const { value, max } = this.document.system.armorScore;
|
||||
const inputValue = Number.parseInt(button.dataset.value);
|
||||
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
|
||||
const changeValue = Math.min(newValue - value, max - value);
|
||||
|
||||
this.document.system.updateArmorValue({ value: changeValue });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -952,6 +936,99 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
});
|
||||
}
|
||||
|
||||
static async #toggleArmorManagement(_event, target) {
|
||||
const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container');
|
||||
if (existingTooltip) {
|
||||
game.tooltip.dismissLockedTooltips();
|
||||
return;
|
||||
}
|
||||
|
||||
const armorSources = getArmorSources(this.document)
|
||||
.filter(s => !s.disabled)
|
||||
.toReversed()
|
||||
.map(({ name, document, data }) => ({
|
||||
...data,
|
||||
uuid: document.uuid,
|
||||
name
|
||||
}));
|
||||
if (!armorSources.length) return;
|
||||
|
||||
const useResourcePips = game.settings.get(
|
||||
CONFIG.DH.id,
|
||||
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||
).useResourcePips;
|
||||
const html = document.createElement('div');
|
||||
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`,
|
||||
{
|
||||
sources: armorSources,
|
||||
useResourcePips
|
||||
}
|
||||
);
|
||||
|
||||
game.tooltip.dismissLockedTooltips();
|
||||
game.tooltip.activate(target, {
|
||||
html,
|
||||
locked: true,
|
||||
cssClass: 'bordered-tooltip',
|
||||
direction: 'DOWN'
|
||||
});
|
||||
|
||||
html.querySelectorAll('.armor-slot').forEach(element => {
|
||||
element.addEventListener('click', CharacterSheet.armorSourcePipUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
static async armorSourceInput(event) {
|
||||
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
|
||||
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
|
||||
event.target.value = value;
|
||||
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
|
||||
progressBar.value = value;
|
||||
}
|
||||
|
||||
/** Update specific armor source */
|
||||
static async armorSourcePipUpdate(event) {
|
||||
const target = event.target.closest('.armor-slot');
|
||||
const { uuid, value } = target.dataset;
|
||||
const document = await foundry.utils.fromUuid(uuid);
|
||||
|
||||
let inputValue = Number.parseInt(value);
|
||||
let decreasing = false;
|
||||
let newCurrent = 0;
|
||||
|
||||
if (document.type === 'armor') {
|
||||
decreasing = document.system.armor.current >= inputValue;
|
||||
newCurrent = decreasing ? inputValue - 1 : inputValue;
|
||||
await document.update({ 'system.armor.current': newCurrent });
|
||||
} else if (document.system.armorData) {
|
||||
const { current } = document.system.armorData;
|
||||
decreasing = current >= inputValue;
|
||||
newCurrent = decreasing ? inputValue - 1 : inputValue;
|
||||
|
||||
const newChanges = document.system.changes.map(change => ({
|
||||
...change,
|
||||
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
|
||||
}));
|
||||
|
||||
await document.update({ 'system.changes': newChanges });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = target.closest('.slot-bar');
|
||||
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
|
||||
const index = Number.parseInt(armorSlot.dataset.index);
|
||||
if (decreasing && index >= newCurrent) {
|
||||
armorSlot.classList.remove('fa-shield');
|
||||
armorSlot.classList.add('fa-shield-halved');
|
||||
} else if (!decreasing && index < newCurrent) {
|
||||
armorSlot.classList.add('fa-shield');
|
||||
armorSlot.classList.remove('fa-shield-halved');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async #toggleResourceManagement(event, button) {
|
||||
event.stopPropagation();
|
||||
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
|
||||
|
|
@ -985,7 +1062,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
|||
);
|
||||
|
||||
const target = button.closest('.resource-section');
|
||||
|
||||
game.tooltip.dismissLockedTooltips();
|
||||
game.tooltip.activate(target, {
|
||||
html,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
refeshActions: Party.#refeshActions,
|
||||
triggerRest: Party.#triggerRest,
|
||||
tagTeamRoll: Party.#tagTeamRoll,
|
||||
groupRoll: Party.#groupRoll,
|
||||
selectRefreshable: DaggerheartMenu.selectRefreshable,
|
||||
refreshActors: DaggerheartMenu.refreshActors
|
||||
groupRoll: Party.#groupRoll
|
||||
},
|
||||
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
|
||||
};
|
||||
|
|
@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
secrets: this.document.isOwner,
|
||||
relativeTo: this.document
|
||||
});
|
||||
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,11 +189,14 @@ export default class Party extends DHBaseActorSheet {
|
|||
* Toggles a armor slot resource value.
|
||||
* @type {ApplicationClickAction}
|
||||
*/
|
||||
static async #toggleArmorSlot(_, target, element) {
|
||||
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
|
||||
const armorValue = Number.parseInt(target.dataset.value);
|
||||
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
|
||||
await armorItem.update({ 'system.marks.value': newValue });
|
||||
static async #toggleArmorSlot(_, target) {
|
||||
const actor = game.actors.get(target.dataset.actorId);
|
||||
const { value, max } = actor.system.armorScore;
|
||||
const inputValue = Number.parseInt(target.dataset.value);
|
||||
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
|
||||
const changeValue = Math.min(newValue - value, max - value);
|
||||
|
||||
await actor.system.updateArmorValue({ value: changeValue });
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
@ -255,11 +257,7 @@ export default class Party extends DHBaseActorSheet {
|
|||
}
|
||||
|
||||
static async #tagTeamRoll() {
|
||||
new game.system.api.applications.dialogs.TagTeamDialog(
|
||||
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
|
||||
).render({
|
||||
force: true
|
||||
});
|
||||
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true });
|
||||
}
|
||||
|
||||
static async #groupRoll(_params) {
|
||||
|
|
|
|||
|
|
@ -749,11 +749,13 @@ export default function DHApplicationMixin(Base) {
|
|||
|
||||
const cls =
|
||||
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
|
||||
|
||||
const data = {
|
||||
name: cls.defaultName({ type, parent }),
|
||||
type,
|
||||
system: systemData
|
||||
};
|
||||
|
||||
if (inVault) data['system.inVault'] = true;
|
||||
if (disabled) data.disabled = true;
|
||||
if (type === 'domainCard' && parent?.system.domains?.length) {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
|
|||
inactives: []
|
||||
};
|
||||
|
||||
for (const effect of this.actor.allApplicableEffects()) {
|
||||
for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) {
|
||||
const list = effect.active ? context.effects.actives : context.effects.inactives;
|
||||
list.push(effect);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
|||
return context;
|
||||
}
|
||||
|
||||
async updateArmorEffect(event) {
|
||||
const value = Number.parseInt(event.target.value);
|
||||
const armorEffect = this.document.system.armorEffect;
|
||||
if (Number.isNaN(value) || !armorEffect) return;
|
||||
|
||||
await armorEffect.system.armorChange.updateArmorMax(value);
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used by `tagifyElement`.
|
||||
* @param {Array<Object>} selectedOptions - The currently selected tag objects.
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs';
|
|||
export { default as DhHotbar } from './hotbar.mjs';
|
||||
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
|
||||
export { ItemBrowser } from './itemBrowser.mjs';
|
||||
export { default as DhProgress } from './progress.mjs';
|
||||
|
|
|
|||
|
|
@ -190,7 +190,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
const target = event.target.closest('[data-die-index]');
|
||||
|
||||
if (target.dataset.type === 'damage') {
|
||||
game.system.api.dice.DamageRoll.reroll(target, message);
|
||||
const { damageType, part, dice, result } = target.dataset;
|
||||
const damagePart = message.system.damage[damageType].parts[part];
|
||||
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
|
||||
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
|
||||
if (index !== Number(part)) return damagePart;
|
||||
return {
|
||||
...damagePart,
|
||||
total: parsedRoll.total,
|
||||
dice: rerolledDice
|
||||
};
|
||||
});
|
||||
const updateMessage = game.messages.get(message._id);
|
||||
await updateMessage.update({
|
||||
[`system.damage.${damageType}`]: {
|
||||
total: parsedRoll.total,
|
||||
parts: damageParts
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
|
||||
const rollClass =
|
||||
|
|
@ -204,20 +221,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
|||
|
||||
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||
|
||||
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
|
||||
const { newRoll, parsedRoll } = await rollClass.reroll(
|
||||
originalRoll_parsed,
|
||||
target.dataset.dieIndex,
|
||||
target.dataset.type
|
||||
);
|
||||
|
||||
await game.messages.get(message._id).update({
|
||||
'system.roll': newRoll,
|
||||
'rolls': [parsedRoll]
|
||||
});
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -253,8 +253,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
for (const item of this.items) {
|
||||
if (['weapon', 'armor'].includes(item.type)) {
|
||||
item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
|
||||
item.system
|
||||
'systems/daggerheart/templates/ui/itemBrowser/item-tags.hbs',
|
||||
{ item: item.system }
|
||||
);
|
||||
}
|
||||
item.system.enrichedDescription =
|
||||
|
|
|
|||
27
module/applications/ui/progress.mjs
Normal file
27
module/applications/ui/progress.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export default class DhProgress {
|
||||
#notification;
|
||||
|
||||
constructor({ max, label = '' }) {
|
||||
this.max = max;
|
||||
this.label = label;
|
||||
this.#notification = ui.notifications.info(this.label, { progress: true });
|
||||
}
|
||||
|
||||
updateMax(newMax) {
|
||||
this.max = newMax;
|
||||
}
|
||||
|
||||
advance({ by = 1, label = this.label } = {}) {
|
||||
if (this.value === this.max) return;
|
||||
this.value = (this.value ?? 0) + Math.abs(by);
|
||||
this.#notification.update({ message: label, pct: this.value / this.max });
|
||||
}
|
||||
|
||||
close({ label = '' } = {}) {
|
||||
this.#notification.update({ message: label, pct: 1 });
|
||||
}
|
||||
|
||||
static createMigrationProgress(max = 0) {
|
||||
return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') });
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
|
|||
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
|
||||
|
||||
const shape = super._createDragShapeData(event);
|
||||
const token = shape?.type === 'emanation' && shape.base?.type === 'token' ? this.#findTokenInBounds(event.interactionData.origin) : null;
|
||||
const token =
|
||||
shape?.type === 'emanation' && shape.base?.type === 'token'
|
||||
? this.#findTokenInBounds(event.interactionData.origin)
|
||||
: null;
|
||||
if (token) {
|
||||
shape.base.width = token.width;
|
||||
shape.base.height = token.height;
|
||||
|
|
|
|||
|
|
@ -959,7 +959,22 @@ export const sceneRangeMeasurementSetting = {
|
|||
}
|
||||
};
|
||||
|
||||
export const activeEffectModes = {
|
||||
export const tagTeamRollTypes = {
|
||||
trait: {
|
||||
id: 'trait',
|
||||
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait'
|
||||
},
|
||||
ability: {
|
||||
id: 'ability',
|
||||
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
|
||||
},
|
||||
damageAbility: {
|
||||
id: 'damageAbility',
|
||||
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
|
||||
}
|
||||
};
|
||||
|
||||
export const baseActiveEffectModes = {
|
||||
custom: {
|
||||
id: 'custom',
|
||||
priority: 0,
|
||||
|
|
@ -997,6 +1012,21 @@ export const activeEffectModes = {
|
|||
}
|
||||
};
|
||||
|
||||
export const activeEffectModes = {
|
||||
armor: {
|
||||
id: 'armor',
|
||||
priority: 20,
|
||||
label: 'TYPES.ActiveEffect.armor'
|
||||
},
|
||||
...baseActiveEffectModes
|
||||
};
|
||||
|
||||
export const activeEffectArmorInteraction = {
|
||||
none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' },
|
||||
active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' },
|
||||
inactive: { id: 'inactive', label: 'DAGGERHEART.CONFIG.ArmorInteraction.inactive.label' }
|
||||
};
|
||||
|
||||
export const activeEffectDurations = {
|
||||
temporary: {
|
||||
id: 'temporary',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const hooksConfig = {
|
||||
effectDisplayToggle: 'DHEffectDisplayToggle',
|
||||
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
|
||||
lockedTooltipDismissed: 'DHLockedTooltipDismissed',
|
||||
tagTeamStart: 'DHTagTeamRollStart'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -489,15 +489,18 @@ export const weaponFeatures = {
|
|||
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
|
||||
img: 'icons/skills/melee/shield-block-bash-blue.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: 'ITEM.@system.tier + 1'
|
||||
},
|
||||
{
|
||||
key: 'system.evasion',
|
||||
mode: 2,
|
||||
value: '-1'
|
||||
},
|
||||
{
|
||||
key: 'Armor',
|
||||
type: 'armor',
|
||||
typeData: {
|
||||
type: 'armor',
|
||||
max: 'ITEM.@system.tier + 1'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -789,11 +792,6 @@ export const weaponFeatures = {
|
|||
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
||||
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
key: 'system.bonuses.damage.primaryWeapon.bonus',
|
||||
mode: 2,
|
||||
|
|
@ -808,6 +806,22 @@ export const weaponFeatures = {
|
|||
type: 'withinRange'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
|
||||
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
|
||||
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'Armor',
|
||||
type: 'armor',
|
||||
value: 0,
|
||||
typeData: {
|
||||
type: 'armor',
|
||||
max: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1191,9 +1205,13 @@ export const weaponFeatures = {
|
|||
img: 'icons/skills/melee/shield-block-gray-orange.webp',
|
||||
changes: [
|
||||
{
|
||||
key: 'system.armorScore',
|
||||
mode: 2,
|
||||
value: 'ITEM.@system.tier'
|
||||
key: 'Armor',
|
||||
type: 'armor',
|
||||
value: 0,
|
||||
typeData: {
|
||||
type: 'armor',
|
||||
max: 'ITEM.@system.tier'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export const gameSettings = {
|
|||
LevelTiers: 'LevelTiers',
|
||||
Countdowns: 'Countdowns',
|
||||
LastMigrationVersion: 'LastMigrationVersion',
|
||||
TagTeamRoll: 'TagTeamRoll',
|
||||
SpotlightRequestQueue: 'SpotlightRequestQueue',
|
||||
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
export { default as DhCombat } from './combat.mjs';
|
||||
export { default as DhCombatant } from './combatant.mjs';
|
||||
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
|
||||
export { default as DhRollTable } from './rollTable.mjs';
|
||||
export { default as RegisteredTriggers } from './registeredTriggers.mjs';
|
||||
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
|
||||
export { default as TagTeamData } from './tagTeamData.mjs';
|
||||
|
||||
export * as countdowns from './countdowns.mjs';
|
||||
export * as actions from './action/_module.mjs';
|
||||
|
|
|
|||
|
|
@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction {
|
|||
|
||||
async use(event, options) {
|
||||
const result = await super.use(event, options);
|
||||
if (!result.message) return;
|
||||
|
||||
if (result.message.system.action.roll?.type === 'attack') {
|
||||
if (result.message?.system.action.roll?.type === 'attack') {
|
||||
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
||||
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,10 +207,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
* @param {Event} event Event from the button used to trigger the Action
|
||||
* @returns {object}
|
||||
*/
|
||||
async use(event) {
|
||||
async use(event, configOptions = {}) {
|
||||
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
|
||||
|
||||
let config = this.prepareConfig(event);
|
||||
let config = this.prepareConfig(event, configOptions);
|
||||
if (!config) return;
|
||||
|
||||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
|
||||
|
|
@ -231,7 +231,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
|
||||
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
|
||||
|
||||
if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
|
||||
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
|
@ -241,7 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
* @param {Event} event Event from the button used to trigger the Action
|
||||
* @returns {object}
|
||||
*/
|
||||
prepareBaseConfig(event) {
|
||||
prepareBaseConfig(event, configOptions = {}) {
|
||||
const isActor = this.item instanceof CONFIG.Actor.documentClass;
|
||||
const actionTitle = game.i18n.localize(this.name);
|
||||
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
|
||||
|
|
@ -268,7 +268,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
data: this.getRollData(),
|
||||
evaluate: this.hasRoll,
|
||||
resourceUpdates: new ResourceUpdateMap(this.actor),
|
||||
targetUuid: this.targetUuid
|
||||
targetUuid: this.targetUuid,
|
||||
...configOptions
|
||||
};
|
||||
|
||||
DHBaseAction.applyKeybindings(config);
|
||||
|
|
@ -280,8 +281,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
* @param {Event} event Event from the button used to trigger the Action
|
||||
* @returns {object}
|
||||
*/
|
||||
prepareConfig(event) {
|
||||
const config = this.prepareBaseConfig(event);
|
||||
prepareConfig(event, configOptions = {}) {
|
||||
const config = this.prepareBaseConfig(event, configOptions);
|
||||
for (const clsField of Object.values(this.schema.fields)) {
|
||||
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
|
||||
}
|
||||
|
|
@ -297,7 +298,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
static async getEffects(actor, effectParent) {
|
||||
if (!actor) return [];
|
||||
|
||||
return Array.from(await actor.allApplicableEffects()).filter(effect => {
|
||||
return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter(
|
||||
effect => {
|
||||
/* Effects on weapons only ever apply for the weapon itself */
|
||||
if (effect.parent.type === 'weapon') {
|
||||
/* Unless they're secondary - then they apply only to other primary weapons */
|
||||
|
|
@ -307,7 +309,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
}
|
||||
|
||||
return !effect.isSuppressed;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -354,11 +357,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
|||
}
|
||||
|
||||
get hasDamage() {
|
||||
return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing';
|
||||
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
|
||||
}
|
||||
|
||||
get hasHealing() {
|
||||
return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing';
|
||||
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
|
||||
}
|
||||
|
||||
get hasSave() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import BaseEffect from './baseEffect.mjs';
|
||||
import BeastformEffect from './beastformEffect.mjs';
|
||||
import HordeEffect from './hordeEffect.mjs';
|
||||
export { changeTypes, changeEffects } from './changeTypes/_module.mjs';
|
||||
|
||||
export { BaseEffect, BeastformEffect, HordeEffect };
|
||||
|
||||
|
|
|
|||
|
|
@ -12,26 +12,41 @@
|
|||
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
|
||||
*/
|
||||
|
||||
import { getScrollTextData } from '../../helpers/utils.mjs';
|
||||
import { changeTypes } from './_module.mjs';
|
||||
|
||||
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
changes: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
const baseChanges = Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, type) => {
|
||||
r[type] = new fields.SchemaField({
|
||||
key: new fields.StringField({ required: true }),
|
||||
type: new fields.StringField({
|
||||
required: true,
|
||||
blank: false,
|
||||
choices: CONFIG.DH.GENERAL.activeEffectModes,
|
||||
initial: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||
choices: [type],
|
||||
initial: type,
|
||||
validate: BaseEffect.#validateType
|
||||
}),
|
||||
value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }),
|
||||
value: new fields.AnyField({
|
||||
required: true,
|
||||
nullable: true,
|
||||
serializable: true,
|
||||
initial: ''
|
||||
}),
|
||||
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
|
||||
priority: new fields.NumberField()
|
||||
})
|
||||
});
|
||||
return r;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...super.defineSchema(),
|
||||
changes: new fields.ArrayField(
|
||||
new fields.TypedSchemaField(
|
||||
{ ...changeTypes, ...baseChanges },
|
||||
{ initial: baseChanges.add.getInitialValue() }
|
||||
)
|
||||
),
|
||||
duration: new fields.SchemaField({
|
||||
type: new fields.StringField({
|
||||
|
|
@ -97,6 +112,23 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
|
|||
return true;
|
||||
}
|
||||
|
||||
get isSuppressed() {
|
||||
for (const change of this.changes) {
|
||||
if (change.isSuppressed) return true;
|
||||
}
|
||||
}
|
||||
|
||||
get armorChange() {
|
||||
return this.changes.find(x => x.type === CONFIG.DH.GENERAL.activeEffectModes.armor.id);
|
||||
}
|
||||
|
||||
get armorData() {
|
||||
const armorChange = this.armorChange;
|
||||
if (!armorChange) return null;
|
||||
|
||||
return armorChange.getArmorData();
|
||||
}
|
||||
|
||||
static getDefaultObject() {
|
||||
return {
|
||||
name: 'New Effect',
|
||||
|
|
@ -116,4 +148,31 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
async _preUpdate(changed, options, userId) {
|
||||
const allowed = await super._preUpdate(changed, options, userId);
|
||||
if (allowed === false) return false;
|
||||
|
||||
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||
if (
|
||||
autoSettings.resourceScrollTexts &&
|
||||
this.parent.actor?.type === 'character' &&
|
||||
this.parent.actor.system.resources.armor
|
||||
) {
|
||||
const newArmorTotal = (changed.system?.changes ?? []).reduce((acc, change) => {
|
||||
if (change.type === 'armor') acc += change.value.current;
|
||||
return acc;
|
||||
}, this.parent.actor.system.armor?.system?.armor?.current ?? 0);
|
||||
|
||||
const armorData = getScrollTextData(this.parent.actor, { value: newArmorTotal }, 'armor');
|
||||
options.scrollingTextData = [armorData];
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate(changed, options, userId) {
|
||||
super._onUpdate(changed, options, userId);
|
||||
|
||||
if (this.parent.actor && options.scrollingTextData)
|
||||
this.parent.actor.queueScrollText(options.scrollingTextData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
module/data/activeEffect/changeTypes/_module.mjs
Normal file
9
module/data/activeEffect/changeTypes/_module.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Armor from './armor.mjs';
|
||||
|
||||
export const changeEffects = {
|
||||
armor: Armor.changeEffect
|
||||
};
|
||||
|
||||
export const changeTypes = {
|
||||
armor: Armor
|
||||
};
|
||||
206
module/data/activeEffect/changeTypes/armor.mjs
Normal file
206
module/data/activeEffect/changeTypes/armor.mjs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
|
||||
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
export default class ArmorChange extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
return {
|
||||
type: new fields.StringField({ required: true, choices: ['armor'], initial: 'armor' }),
|
||||
priority: new fields.NumberField(),
|
||||
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
|
||||
value: new fields.SchemaField({
|
||||
current: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
|
||||
max: new fields.StringField({
|
||||
required: true,
|
||||
nullable: false,
|
||||
initial: '1',
|
||||
label: 'DAGGERHEART.GENERAL.max'
|
||||
}),
|
||||
damageThresholds: new fields.SchemaField(
|
||||
{
|
||||
major: new fields.StringField({
|
||||
initial: '0',
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
||||
}),
|
||||
severe: new fields.StringField({
|
||||
initial: '0',
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||
})
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
interaction: new fields.StringField({
|
||||
required: true,
|
||||
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
|
||||
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
|
||||
label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.label',
|
||||
hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.hint'
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
static changeEffect = {
|
||||
label: 'Armor',
|
||||
defaultPriority: 20,
|
||||
handler: (actor, change, _options, _field, replacementData) => {
|
||||
const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
|
||||
game.system.api.documents.DhActiveEffect.applyChange(
|
||||
actor,
|
||||
{
|
||||
...change,
|
||||
key: 'system.armorScore.value',
|
||||
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||
value: change.value.current
|
||||
},
|
||||
replacementData
|
||||
);
|
||||
game.system.api.documents.DhActiveEffect.applyChange(
|
||||
actor,
|
||||
{
|
||||
...change,
|
||||
key: 'system.armorScore.max',
|
||||
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
|
||||
value: parsedMax
|
||||
},
|
||||
replacementData
|
||||
);
|
||||
|
||||
if (change.value.damageThresholds) {
|
||||
const getThresholdValue = value => {
|
||||
const parsed = itemAbleRollParse(value, actor, change.effect.parent);
|
||||
const roll = new Roll(parsed).evaluateSync();
|
||||
return roll ? (roll.isDeterministic ? roll.total : null) : null;
|
||||
};
|
||||
const major = getThresholdValue(change.value.damageThresholds.major);
|
||||
const severe = getThresholdValue(change.value.damageThresholds.severe);
|
||||
|
||||
if (major) {
|
||||
game.system.api.documents.DhActiveEffect.applyChange(
|
||||
actor,
|
||||
{
|
||||
...change,
|
||||
key: 'system.damageThresholds.major',
|
||||
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
|
||||
priority: 50,
|
||||
value: major
|
||||
},
|
||||
replacementData
|
||||
);
|
||||
}
|
||||
|
||||
if (severe) {
|
||||
game.system.api.documents.DhActiveEffect.applyChange(
|
||||
actor,
|
||||
{
|
||||
...change,
|
||||
key: 'system.damageThresholds.severe',
|
||||
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
|
||||
priority: 50,
|
||||
value: severe
|
||||
},
|
||||
replacementData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
render: null
|
||||
};
|
||||
|
||||
get isSuppressed() {
|
||||
switch (this.value.interaction) {
|
||||
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
|
||||
return !this.parent.parent?.actor.system.armor;
|
||||
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id:
|
||||
return Boolean(this.parent.parent?.actor.system.armor);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static getInitialValue() {
|
||||
return {
|
||||
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
|
||||
value: {
|
||||
current: 0,
|
||||
max: 0
|
||||
},
|
||||
phase: 'initial',
|
||||
priority: 20
|
||||
};
|
||||
}
|
||||
|
||||
static getDefaultArmorEffect() {
|
||||
return {
|
||||
name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'),
|
||||
img: 'icons/equipment/chest/breastplate-helmet-metal.webp',
|
||||
system: {
|
||||
changes: [ArmorChange.getInitialValue()]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
|
||||
getArmorData() {
|
||||
const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null;
|
||||
const maxParse = actor ? itemAbleRollParse(this.value.max, actor, this.parent.parent.parent) : null;
|
||||
const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null;
|
||||
const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null;
|
||||
|
||||
return {
|
||||
current: this.value.current,
|
||||
max: maxEvaluated ?? this.value.max
|
||||
};
|
||||
}
|
||||
|
||||
async updateArmorMax(newMax) {
|
||||
const newChanges = [
|
||||
...this.parent.changes.map(change => ({
|
||||
...change,
|
||||
value:
|
||||
change.type === 'armor'
|
||||
? {
|
||||
...change.value,
|
||||
current: Math.min(change.value.current, newMax),
|
||||
max: newMax
|
||||
}
|
||||
: change.value
|
||||
}))
|
||||
];
|
||||
await this.parent.parent.update({ 'system.changes': newChanges });
|
||||
}
|
||||
|
||||
static orderEffectsForAutoChange(armorEffects, increasing) {
|
||||
const getEffectWeight = effect => {
|
||||
switch (effect.parent.type) {
|
||||
case 'class':
|
||||
case 'subclass':
|
||||
case 'ancestry':
|
||||
case 'community':
|
||||
case 'feature':
|
||||
case 'domainCard':
|
||||
return 2;
|
||||
case 'armor':
|
||||
return 3;
|
||||
case 'loot':
|
||||
case 'consumable':
|
||||
return 4;
|
||||
case 'weapon':
|
||||
return 5;
|
||||
case 'character':
|
||||
return 6;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
return armorEffects
|
||||
.filter(x => !x.disabled && !x.isSuppressed)
|
||||
.sort((a, b) =>
|
||||
increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -189,19 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
|||
return true;
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
/* Clear all partyMembers from tagTeam setting.*/
|
||||
/* Revisit this when tagTeam is improved for many parties */
|
||||
if (this.parent.parties.size > 0) {
|
||||
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
await tagTeam.updateSource({
|
||||
initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator,
|
||||
members: Object.keys(tagTeam.members).find(x => x === this.parent.id) ? { [this.parent.id]: _del } : {}
|
||||
});
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
|
||||
}
|
||||
}
|
||||
|
||||
async _preUpdate(changes, options, userId) {
|
||||
const allowed = await super._preUpdate(changes, options, userId);
|
||||
if (allowed === false) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import DhCreature from './creature.mjs';
|
|||
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
|
||||
import { ActionField } from '../fields/actionField.mjs';
|
||||
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
|
||||
import { getArmorSources } from '../../helpers/utils.mjs';
|
||||
|
||||
export default class DhCharacter extends DhCreature {
|
||||
/**@override */
|
||||
|
|
@ -41,17 +42,16 @@ export default class DhCharacter extends DhCreature {
|
|||
label: 'DAGGERHEART.GENERAL.proficiency'
|
||||
}),
|
||||
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
|
||||
armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }),
|
||||
damageThresholds: new fields.SchemaField({
|
||||
severe: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||
}),
|
||||
major: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
|
||||
}),
|
||||
severe: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 0,
|
||||
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
|
||||
})
|
||||
}),
|
||||
experiences: new fields.TypedObjectField(
|
||||
|
|
@ -465,6 +465,101 @@ export default class DhCharacter extends DhCreature {
|
|||
}
|
||||
}
|
||||
|
||||
async updateArmorValue({ value: armorChange = 0, clear = false }) {
|
||||
if (armorChange === 0 && !clear) return;
|
||||
|
||||
const increasing = armorChange >= 0;
|
||||
let remainingChange = Math.abs(armorChange);
|
||||
const orderedSources = getArmorSources(this.parent).filter(s => !s.disabled);
|
||||
|
||||
const handleArmorData = (embeddedUpdates, doc, armorData) => {
|
||||
let usedArmorChange = 0;
|
||||
if (clear) {
|
||||
usedArmorChange -= armorData.current;
|
||||
} else {
|
||||
if (increasing) {
|
||||
const remainingArmor = armorData.max - armorData.current;
|
||||
usedArmorChange = Math.min(remainingChange, remainingArmor);
|
||||
remainingChange -= usedArmorChange;
|
||||
} else {
|
||||
const changeChange = Math.min(armorData.current, remainingChange);
|
||||
usedArmorChange -= changeChange;
|
||||
remainingChange -= changeChange;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usedArmorChange) return usedArmorChange;
|
||||
else {
|
||||
if (!embeddedUpdates[doc.id]) embeddedUpdates[doc.id] = { doc: doc, updates: [] };
|
||||
|
||||
return usedArmorChange;
|
||||
}
|
||||
};
|
||||
|
||||
const armorUpdates = [];
|
||||
const effectUpdates = [];
|
||||
for (const { document: armorSource } of orderedSources) {
|
||||
const usedArmorChange = handleArmorData(
|
||||
armorSource.type === 'armor' ? armorUpdates : effectUpdates,
|
||||
armorSource.parent,
|
||||
armorSource.type === 'armor' ? armorSource.system.armor : armorSource.system.armorData
|
||||
);
|
||||
if (!usedArmorChange) continue;
|
||||
|
||||
if (armorSource.type === 'armor') {
|
||||
armorUpdates[armorSource.parent.id].updates.push({
|
||||
'_id': armorSource.id,
|
||||
'system.armor.current': armorSource.system.armor.current + usedArmorChange
|
||||
});
|
||||
} else {
|
||||
effectUpdates[armorSource.parent.id].updates.push({
|
||||
'_id': armorSource.id,
|
||||
'system.changes': armorSource.system.changes.map(change => ({
|
||||
...change,
|
||||
value:
|
||||
change.type === 'armor'
|
||||
? {
|
||||
...change.value,
|
||||
current: armorSource.system.armorChange.value.current + usedArmorChange
|
||||
}
|
||||
: change.value
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (remainingChange === 0 && !clear) break;
|
||||
}
|
||||
|
||||
const armorUpdateValues = Object.values(armorUpdates);
|
||||
for (const [index, { doc, updates }] of armorUpdateValues.entries())
|
||||
await doc.updateEmbeddedDocuments('Item', updates, { render: index === armorUpdateValues.length - 1 });
|
||||
|
||||
const effectUpdateValues = Object.values(effectUpdates);
|
||||
for (const [index, { doc, updates }] of effectUpdateValues.entries())
|
||||
await doc.updateEmbeddedDocuments('ActiveEffect', updates, {
|
||||
render: index === effectUpdateValues.length - 1
|
||||
});
|
||||
}
|
||||
|
||||
async updateArmorEffectValue({ uuid, value }) {
|
||||
const source = await foundry.utils.fromUuid(uuid);
|
||||
if (source.type === 'armor') {
|
||||
await source.update({
|
||||
'system.armor.current': source.system.armor.current + value
|
||||
});
|
||||
} else {
|
||||
const effectValue = source.system.armorChange.value;
|
||||
await source.update({
|
||||
'system.changes': [
|
||||
{
|
||||
...source.system.armorChange,
|
||||
value: { ...effectValue, current: effectValue.current + value }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get sheetLists() {
|
||||
const ancestryFeatures = [],
|
||||
communityFeatures = [],
|
||||
|
|
@ -588,6 +683,10 @@ export default class DhCharacter extends DhCreature {
|
|||
|
||||
prepareBaseData() {
|
||||
super.prepareBaseData();
|
||||
this.armorScore = {
|
||||
max: this.armor?.system.armor.max ?? 0,
|
||||
value: this.armor?.system.armor.current ?? 0
|
||||
};
|
||||
this.evasion += this.class.value?.system?.evasion ?? 0;
|
||||
|
||||
const currentLevel = this.levelData.level.current;
|
||||
|
|
@ -637,14 +736,12 @@ export default class DhCharacter extends DhCreature {
|
|||
}
|
||||
}
|
||||
|
||||
const armor = this.armor;
|
||||
this.armorScore = armor ? armor.system.baseScore : 0;
|
||||
this.damageThresholds = {
|
||||
major: armor
|
||||
? armor.system.baseThresholds.major + this.levelData.level.current
|
||||
major: this.armor
|
||||
? this.armor.system.baseThresholds.major + this.levelData.level.current
|
||||
: this.levelData.level.current,
|
||||
severe: armor
|
||||
? armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
severe: this.armor
|
||||
? this.armor.system.baseThresholds.severe + this.levelData.level.current
|
||||
: this.levelData.level.current * 2
|
||||
};
|
||||
|
||||
|
|
@ -679,9 +776,8 @@ export default class DhCharacter extends DhCreature {
|
|||
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
|
||||
|
||||
this.resources.armor = {
|
||||
...this.armorScore,
|
||||
label: 'DAGGERHEART.GENERAL.armor',
|
||||
value: this.armor?.system?.marks?.value ?? 0,
|
||||
max: this.armorScore,
|
||||
isReversed: true
|
||||
};
|
||||
|
||||
|
|
@ -757,7 +853,6 @@ export default class DhCharacter extends DhCreature {
|
|||
|
||||
static migrateData(source) {
|
||||
if (typeof source.scars === 'object') source.scars = 0;
|
||||
if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0);
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,10 +75,6 @@ export default class DhEnvironment extends BaseDataActor {
|
|||
);
|
||||
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
|
||||
game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: { refreshType: RefreshType.TagTeamRoll }
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import BaseDataActor from './base.mjs';
|
||||
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||
import TagTeamData from '../tagTeamData.mjs';
|
||||
|
||||
export default class DhParty extends BaseDataActor {
|
||||
/**@inheritdoc */
|
||||
|
|
@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor {
|
|||
handfuls: new fields.NumberField({ initial: 1, integer: true }),
|
||||
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
}),
|
||||
tagTeam: new fields.EmbeddedDataField(TagTeamData)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -40,23 +42,6 @@ export default class DhParty extends BaseDataActor {
|
|||
}
|
||||
}
|
||||
|
||||
async _preDelete() {
|
||||
/* Clear all partyMembers from tagTeam setting.*/
|
||||
/* Revisit this when tagTeam is improved for many parties */
|
||||
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
await tagTeam.updateSource({
|
||||
initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator,
|
||||
members: Object.keys(tagTeam.members).reduce((acc, key) => {
|
||||
if (this.partyMembers.find(x => x.id === key)) {
|
||||
acc[key] = _del;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
});
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
|
||||
}
|
||||
|
||||
_onDelete(options, userId) {
|
||||
super._onDelete(options, userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export default class DamageField extends fields.SchemaField {
|
|||
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
||||
|
||||
const damageConfig = {
|
||||
dialog: {},
|
||||
...config,
|
||||
roll: formulas,
|
||||
dialog: {},
|
||||
data: this.getRollData()
|
||||
};
|
||||
delete damageConfig.evaluate;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField {
|
|||
static async execute(config, targets = null, force = false) {
|
||||
if (!config.hasEffect) return;
|
||||
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||
if (!message) {
|
||||
if (!message && !config.skips.createMessage) {
|
||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||
roll._evaluated = true;
|
||||
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField {
|
|||
if (!config.hasSave) return;
|
||||
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||
|
||||
if (!message) {
|
||||
if (!message && !config.skips.createMessage) {
|
||||
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||
roll._evaluated = true;
|
||||
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||
|
|
|
|||
|
|
@ -82,6 +82,24 @@ class ResourcesField extends fields.TypedObjectField {
|
|||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Foundry bar attributes are unable to handle finding the schema field nor the label normally.
|
||||
* This returns the element if its a valid resource key and overwrites the element's label for that retrieval.
|
||||
*/
|
||||
_getField(path) {
|
||||
if (path.length === 0) return this;
|
||||
const first = path.shift();
|
||||
if (first === this.element.name) return this.element_getField(path);
|
||||
|
||||
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
|
||||
if (first in resources) {
|
||||
this.element.label = resources[first].label;
|
||||
return this.element._getField(path);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default class IterableTypedObjectField extends foundry.data.fields.TypedO
|
|||
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
|
||||
*/
|
||||
const IterableObjectPrototype = {
|
||||
[Symbol.iterator]: function*() {
|
||||
[Symbol.iterator]: function* () {
|
||||
for (const value of Object.values(this)) {
|
||||
yield value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,14 @@ export default class DHArmor extends AttachableItem {
|
|||
...super.defineSchema(),
|
||||
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
armor: new fields.SchemaField({
|
||||
current: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
|
||||
max: new fields.NumberField({ required: true, integer: true, initial: 0 })
|
||||
}),
|
||||
baseThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
severe: new fields.NumberField({ integer: true, initial: 0 })
|
||||
}),
|
||||
armorFeatures: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
value: new fields.StringField({
|
||||
|
|
@ -28,14 +35,7 @@ export default class DHArmor extends AttachableItem {
|
|||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
|
||||
})
|
||||
),
|
||||
marks: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
baseThresholds: new fields.SchemaField({
|
||||
major: new fields.NumberField({ integer: true, initial: 0 }),
|
||||
severe: new fields.NumberField({ integer: true, initial: 0 })
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -151,13 +151,20 @@ export default class DHArmor extends AttachableItem {
|
|||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateDocumentData(source) {
|
||||
if (!source.system.armor) {
|
||||
source.system.armor = { current: source.system.marks?.value ?? 0, max: source.system.baseScore ?? 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of localized tags based on this item's type-specific properties.
|
||||
* @returns {string[]} An array of localized tag strings.
|
||||
*/
|
||||
_getTags() {
|
||||
const tags = [
|
||||
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`,
|
||||
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`,
|
||||
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}`
|
||||
];
|
||||
|
||||
|
|
@ -169,9 +176,7 @@ export default class DHArmor extends AttachableItem {
|
|||
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
|
||||
*/
|
||||
_getLabels() {
|
||||
const labels = [];
|
||||
if (this.baseScore)
|
||||
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
|
||||
const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`];
|
||||
return labels;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -222,9 +222,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
|||
|
||||
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
||||
const armorChanged =
|
||||
changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value;
|
||||
changed.system?.armor?.current !== undefined && changed.system.armor.current !== this.armor.current;
|
||||
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
|
||||
const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor');
|
||||
const armorChangeValue = changed.system.armor.current - this.armor.current;
|
||||
const armorData = getScrollTextData(
|
||||
this.parent.parent,
|
||||
{ value: armorChangeValue + this.parent.parent.system.armorScore.value },
|
||||
'armor'
|
||||
);
|
||||
options.scrollingTextData = [armorData];
|
||||
}
|
||||
|
||||
|
|
|
|||
47
module/data/tagTeamData.mjs
Normal file
47
module/data/tagTeamData.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
export default class TagTeamData extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
initiator: new fields.SchemaField(
|
||||
{
|
||||
memberId: new fields.StringField({
|
||||
required: true,
|
||||
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.memberId.label'
|
||||
}),
|
||||
cost: new fields.NumberField({
|
||||
integer: true,
|
||||
initial: 3,
|
||||
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.cost.label'
|
||||
})
|
||||
},
|
||||
{ nullable: true, initial: null }
|
||||
),
|
||||
members: new fields.TypedObjectField(new fields.EmbeddedDataField(MemberData))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MemberData extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
name: new fields.StringField({ required: true }),
|
||||
img: new fields.StringField({ required: true }),
|
||||
rollType: new fields.StringField({
|
||||
required: true,
|
||||
choices: CONFIG.DH.GENERAL.tagTeamRollTypes,
|
||||
initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id,
|
||||
label: 'Roll Type'
|
||||
}),
|
||||
rollChoice: new fields.StringField({ nullable: true, initial: null }),
|
||||
rollData: new fields.JSONField({ nullable: true, initial: null }),
|
||||
selected: new fields.BooleanField({ initial: false })
|
||||
};
|
||||
}
|
||||
|
||||
get roll() {
|
||||
return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { DhCharacter } from './actor/_module.mjs';
|
||||
|
||||
export default class DhTagTeamRoll extends foundry.abstract.DataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
return {
|
||||
initiator: new fields.SchemaField({
|
||||
id: new fields.StringField({ nullable: true, initial: null }),
|
||||
cost: new fields.NumberField({ integer: true, min: 0, initial: 3 })
|
||||
}),
|
||||
members: new fields.TypedObjectField(
|
||||
new fields.SchemaField({
|
||||
messageId: new fields.StringField({ required: true, nullable: true, initial: null }),
|
||||
selected: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
|
||||
import { parseRallyDice } from '../helpers/utils.mjs';
|
||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
||||
import DHRoll from './dhRoll.mjs';
|
||||
|
||||
export default class DamageRoll extends DHRoll {
|
||||
|
|
@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll {
|
|||
return mods;
|
||||
}
|
||||
|
||||
static async reroll(target, message) {
|
||||
const { damageType, part, dice, result } = target.dataset;
|
||||
const rollPart = message.system.damage[damageType].parts[part];
|
||||
|
||||
static async reroll(rollPart, dice, result) {
|
||||
let diceIndex = 0;
|
||||
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
|
||||
...rollPart.roll,
|
||||
|
|
@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll {
|
|||
};
|
||||
});
|
||||
|
||||
const updateMessage = game.messages.get(message._id);
|
||||
const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => {
|
||||
if (index !== Number(part)) return damagePart;
|
||||
return {
|
||||
...rollPart,
|
||||
total: parsedRoll.total,
|
||||
dice: rerolledDice
|
||||
};
|
||||
});
|
||||
await updateMessage.update({
|
||||
[`system.damage.${damageType}`]: {
|
||||
...updateMessage,
|
||||
total: parsedRoll.total,
|
||||
parts: damageParts
|
||||
}
|
||||
});
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
return { parsedRoll, rerolledDice };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ export default class DHRoll extends Roll {
|
|||
static async build(config = {}, message = {}) {
|
||||
const roll = await this.buildConfigure(config, message);
|
||||
if (!roll) return;
|
||||
|
||||
if (config.skips?.createMessage) config.messageRoll = roll;
|
||||
|
||||
await this.buildEvaluate(roll, config, (message = {}));
|
||||
await this.buildPost(roll, config, (message = {}));
|
||||
return config;
|
||||
|
|
@ -30,12 +33,6 @@ export default class DHRoll extends Roll {
|
|||
config.hooks = [...this.getHooks(), ''];
|
||||
config.dialog ??= {};
|
||||
|
||||
const actorIdSplit = config.source?.actor?.split('.');
|
||||
if (actorIdSplit) {
|
||||
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
config.tagTeamSelected = Boolean(tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]]);
|
||||
}
|
||||
|
||||
for (const hook of config.hooks) {
|
||||
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||
}
|
||||
|
|
@ -146,6 +143,7 @@ export default class DHRoll extends Roll {
|
|||
return foundry.applications.handlebars.renderTemplate(template, {
|
||||
...chatData,
|
||||
parent: chatData.parent,
|
||||
targetMode: chatData.targetMode,
|
||||
metagamingSettings
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll {
|
|||
}
|
||||
}
|
||||
|
||||
static async reroll(rollString, target, message) {
|
||||
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
|
||||
const term = parsedRoll.terms[target.dataset.dieIndex];
|
||||
static async reroll(rollBase, dieIndex, diceType) {
|
||||
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false });
|
||||
const term = parsedRoll.terms[dieIndex];
|
||||
await term.reroll(`/r1=${term.total}`);
|
||||
const result = await parsedRoll.evaluate();
|
||||
|
||||
|
|
@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll {
|
|||
options: { appearance: {} }
|
||||
};
|
||||
|
||||
const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
|
||||
const type = target.dataset.type;
|
||||
if (diceSoNicePresets[type]) {
|
||||
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
|
||||
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
|
||||
if (diceSoNicePresets[diceType]) {
|
||||
diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType];
|
||||
}
|
||||
|
||||
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
|
||||
} else {
|
||||
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
|
||||
}
|
||||
|
||||
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
|
||||
targets: message.system.targets,
|
||||
targets: parsedRoll.options.targets ?? [],
|
||||
roll: {
|
||||
advantage: message.system.roll.advantage?.type,
|
||||
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
|
||||
advantage: parsedRoll.options.roll.advantage?.type,
|
||||
difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null
|
||||
}
|
||||
});
|
||||
|
||||
const extraIndex = newRoll.advantage ? 3 : 2;
|
||||
newRoll.extra = newRoll.extra.slice(extraIndex);
|
||||
|
||||
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
|
||||
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
|
||||
const actor = parsedRoll.options.source.actor
|
||||
? await foundry.utils.fromUuid(parsedRoll.options.source.actor)
|
||||
: null;
|
||||
const config = {
|
||||
source: { actor: message.system.source.actor ?? '' },
|
||||
targets: message.system.targets,
|
||||
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
|
||||
source: { actor: parsedRoll.options.source.actor ?? '' },
|
||||
targets: parsedRoll.targets,
|
||||
roll: newRoll,
|
||||
rerolledRoll: message.system.roll,
|
||||
rerolledRoll: parsedRoll.roll,
|
||||
resourceUpdates: new ResourceUpdateMap(actor)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
|
||||
/**@override */
|
||||
get isSuppressed() {
|
||||
if (this.system.isSuppressed === true) return true;
|
||||
|
||||
// If this is a copied effect from an attachment, never suppress it
|
||||
// (These effects have attachmentSource metadata)
|
||||
if (this.flags?.daggerheart?.attachmentSource) {
|
||||
|
|
@ -15,7 +17,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
}
|
||||
|
||||
// Then apply the standard suppression rules
|
||||
if (['weapon', 'armor'].includes(this.parent?.type)) {
|
||||
if (['weapon', 'armor'].includes(this.parent?.type) && this.transfer) {
|
||||
return !this.parent.system.equipped;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
throw new Error('The array of sub-types to restrict to must not be empty.');
|
||||
}
|
||||
|
||||
const creatableEffects = ['base'];
|
||||
const creatableEffects = types || ['base'];
|
||||
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
|
||||
const labelKey = `TYPES.ActiveEffect.${type}`;
|
||||
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;
|
||||
|
|
@ -175,9 +177,9 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
super.applyChangeField(model, change, field);
|
||||
}
|
||||
|
||||
_applyLegacy(actor, change, changes) {
|
||||
static _applyChangeUnguided(actor, change, changes, options) {
|
||||
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
|
||||
super._applyLegacy(actor, change, changes);
|
||||
super._applyChangeUnguided(actor, change, changes, options);
|
||||
}
|
||||
|
||||
static getChangeValue(model, change, effect) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs';
|
|||
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
|
||||
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||
import { abilities } from '../config/actorConfig.mjs';
|
||||
|
||||
export default class DhpActor extends Actor {
|
||||
parties = new Set();
|
||||
|
|
@ -509,6 +510,30 @@ export default class DhpActor extends Actor {
|
|||
return await rollClass.build(config);
|
||||
}
|
||||
|
||||
async rollTrait(trait, options = {}) {
|
||||
const abilityLabel = game.i18n.localize(abilities[trait].label);
|
||||
const config = {
|
||||
event: event,
|
||||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this),
|
||||
roll: {
|
||||
trait: trait,
|
||||
type: 'trait'
|
||||
},
|
||||
hasRoll: true,
|
||||
actionType: 'action',
|
||||
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
...options
|
||||
};
|
||||
return await this.diceRoll(config);
|
||||
}
|
||||
|
||||
get rollClass() {
|
||||
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
|
||||
}
|
||||
|
|
@ -573,8 +598,7 @@ export default class DhpActor extends Actor {
|
|||
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
|
||||
|
||||
const canUseArmor =
|
||||
this.system.armor &&
|
||||
this.system.armor.system.marks.value < this.system.armorScore &&
|
||||
this.system.armorScore.value < this.system.armorScore.max &&
|
||||
type.every(t => this.system.armorApplicableDamageTypes[t] === true);
|
||||
const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
|
||||
const rule = stressDamageReduction[x];
|
||||
|
|
@ -614,12 +638,7 @@ export default class DhpActor extends Actor {
|
|||
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||
if (hpDamage?.value) {
|
||||
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
||||
if (
|
||||
this.type === 'character' &&
|
||||
!isDirect &&
|
||||
this.system.armor &&
|
||||
this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)
|
||||
) {
|
||||
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
|
||||
const armorSlotResult = await this.owner.query(
|
||||
'armorSlot',
|
||||
{
|
||||
|
|
@ -632,12 +651,10 @@ export default class DhpActor extends Actor {
|
|||
}
|
||||
);
|
||||
if (armorSlotResult) {
|
||||
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult;
|
||||
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
|
||||
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
|
||||
if (armorSpent) {
|
||||
const armorUpdate = updates.find(u => u.key === 'armor');
|
||||
if (armorUpdate) armorUpdate.value += armorSpent;
|
||||
else updates.push({ value: armorSpent, key: 'armor' });
|
||||
for (const armorChange of armorChanges) {
|
||||
updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
|
||||
}
|
||||
if (stressSpent) {
|
||||
const stressUpdate = updates.find(u => u.key === 'stress');
|
||||
|
|
@ -784,12 +801,8 @@ export default class DhpActor extends Actor {
|
|||
);
|
||||
break;
|
||||
case 'armor':
|
||||
if (this.system.armor?.system?.marks) {
|
||||
updates.armor.resources['system.marks.value'] = Math.max(
|
||||
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
|
||||
0
|
||||
);
|
||||
}
|
||||
if (!r.uuid) this.system.updateArmorValue(r);
|
||||
else this.system.updateArmorEffectValue(r);
|
||||
break;
|
||||
default:
|
||||
if (this.system.resources?.[r.key]) {
|
||||
|
|
@ -1005,4 +1018,20 @@ export default class DhpActor extends Actor {
|
|||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
*allApplicableEffects({ noSelfArmor, noTransferArmor } = {}) {
|
||||
for (const effect of this.effects) {
|
||||
if (!noSelfArmor || effect.type !== 'armor') yield effect;
|
||||
}
|
||||
for (const item of this.items) {
|
||||
for (const effect of item.effects) {
|
||||
if (effect.transfer && (!noTransferArmor || effect.type !== 'armor')) yield effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyActiveEffects(phase) {
|
||||
super.applyActiveEffects(phase);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
|
||||
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
|
||||
}
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onApplyDamage(event) {
|
||||
|
|
|
|||
|
|
@ -230,4 +230,14 @@ export default class DHItem extends foundry.documents.Item {
|
|||
async _preDelete() {
|
||||
this.deleteTriggers();
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
const documentClass = game.system.api.data.items[`DH${source.type?.capitalize()}`];
|
||||
if (documentClass?.migrateDocumentData) {
|
||||
documentClass.migrateDocumentData(source);
|
||||
}
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
|
|||
if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip();
|
||||
}
|
||||
|
||||
_setAnchor(direction, options) {
|
||||
_setAnchor(direction, options = {}) {
|
||||
const directions = this.constructor.TOOLTIP_DIRECTIONS;
|
||||
const pad = this.constructor.TOOLTIP_MARGIN_PX;
|
||||
const pos = this.element.getBoundingClientRect();
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ export default class RegisterHandlebarsHelpers {
|
|||
}
|
||||
|
||||
static damageSymbols(damageParts) {
|
||||
const symbols = [...new Set(damageParts.map(x => x.type))].map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
|
||||
const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))];
|
||||
const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
|
||||
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -528,7 +528,8 @@ export function expireActiveEffects(actor, allowedTypes = null) {
|
|||
|
||||
export async function getCritDamageBonus(formula) {
|
||||
const critRoll = new Roll(formula);
|
||||
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
|
||||
await critRoll.evaluate();
|
||||
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0);
|
||||
}
|
||||
|
||||
export function htmlToText(html) {
|
||||
|
|
@ -742,3 +743,67 @@ export function getUnusedDamageTypes(parts) {
|
|||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** Returns resolved armor sources ordered by application order */
|
||||
export function getArmorSources(actor) {
|
||||
const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData);
|
||||
if (actor.system.armor) rawArmorSources.push(actor.system.armor);
|
||||
|
||||
const data = rawArmorSources.map(doc => {
|
||||
// Get the origin item. Since the actor is already loaded, it should already be cached
|
||||
// Consider the relative function versions if this causes an issue
|
||||
const isItem = doc instanceof Item;
|
||||
const origin = isItem ? doc : doc.origin ? foundry.utils.fromUuidSync(doc.origin) : doc.parent;
|
||||
return {
|
||||
origin,
|
||||
name: origin.name,
|
||||
document: doc,
|
||||
data: doc.system.armor ?? doc.system.armorData,
|
||||
disabled: !!doc.disabled || !!doc.isSuppressed
|
||||
};
|
||||
});
|
||||
|
||||
return sortBy(data, ({ origin }) => {
|
||||
switch (origin?.type) {
|
||||
case 'class':
|
||||
case 'subclass':
|
||||
case 'ancestry':
|
||||
case 'community':
|
||||
case 'feature':
|
||||
case 'domainCard':
|
||||
return 2;
|
||||
case 'loot':
|
||||
case 'consumable':
|
||||
return 3;
|
||||
case 'character':
|
||||
return 4;
|
||||
case 'weapon':
|
||||
return 5;
|
||||
case 'armor':
|
||||
return 6;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array sorted by a function that returns a thing to compare, or an array to compare in order
|
||||
* Similar to lodash's sortBy function.
|
||||
*/
|
||||
export function sortBy(arr, fn) {
|
||||
const directCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
const cmp = (a, b) => {
|
||||
const resultA = fn(a);
|
||||
const resultB = fn(b);
|
||||
if (Array.isArray(resultA) && Array.isArray(resultB)) {
|
||||
for (let idx = 0; idx < Math.min(resultA.length, resultB.length); idx++) {
|
||||
const result = directCompare(resultA[idx], resultB[idx]);
|
||||
if (result !== 0) return result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return directCompare(resultA, resultB);
|
||||
};
|
||||
return arr.sort(cmp);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
|
||||
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
|
||||
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
|
||||
'systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs',
|
||||
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
|
||||
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
|
||||
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',
|
||||
|
|
@ -47,6 +48,7 @@ export const preloadHandlebarsTemplates = async function () {
|
|||
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
|
||||
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
|
||||
'systems/daggerheart/templates/scene/dh-config.hbs',
|
||||
'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs'
|
||||
'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs',
|
||||
'systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs'
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export async function runMigrations() {
|
|||
}
|
||||
|
||||
if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) {
|
||||
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
||||
const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll');
|
||||
const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator);
|
||||
const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => {
|
||||
if (!game.actors.some(actor => actor.id === id)) {
|
||||
|
|
@ -206,7 +206,7 @@ export async function runMigrations() {
|
|||
initiator: initatorMissing ? null : tagTeam.initiator,
|
||||
members: missingMembers
|
||||
});
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
|
||||
await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam);
|
||||
|
||||
lastMigrationVersion = '1.2.7';
|
||||
}
|
||||
|
|
@ -246,6 +246,101 @@ export async function runMigrations() {
|
|||
|
||||
lastMigrationVersion = '1.6.0';
|
||||
}
|
||||
|
||||
if (foundry.utils.isNewerVersion('2.0.0', lastMigrationVersion)) {
|
||||
const progress = game.system.api.applications.ui.DhProgress.createMigrationProgress(0);
|
||||
const progressBuffer = 50;
|
||||
|
||||
//#region Data Setup
|
||||
const lockedPacks = [];
|
||||
const itemPacks = game.packs.filter(x => x.metadata.type === 'Item');
|
||||
const actorPacks = game.packs.filter(x => x.metadata.type === 'Actor');
|
||||
|
||||
const getIndexes = async (packs, type) => {
|
||||
const indexes = [];
|
||||
for (const pack of packs) {
|
||||
const indexValues = pack.index.values().reduce((acc, index) => {
|
||||
if (!type || index.type === type) acc.push(index.uuid);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (indexValues.length && pack.locked) {
|
||||
lockedPacks.push(pack.collection);
|
||||
await pack.configure({ locked: false });
|
||||
}
|
||||
|
||||
indexes.push(...indexValues);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
};
|
||||
|
||||
const itemEntries = await getIndexes(itemPacks);
|
||||
const characterEntries = await getIndexes(actorPacks, 'character');
|
||||
|
||||
const worldItems = game.items;
|
||||
const worldCharacters = game.actors.filter(x => x.type === 'character');
|
||||
|
||||
/* The async fetches are the mainstay of time. Leaving 1 progress for the sync logic */
|
||||
const newMax = itemEntries.length + characterEntries.length + progressBuffer;
|
||||
progress.updateMax(newMax);
|
||||
|
||||
const compendiumItems = [];
|
||||
for (const entry of itemEntries) {
|
||||
const item = await foundry.utils.fromUuid(entry);
|
||||
compendiumItems.push(item);
|
||||
progress.advance();
|
||||
}
|
||||
|
||||
const compendiumCharacters = [];
|
||||
for (const entry of characterEntries) {
|
||||
const character = await foundry.utils.fromUuid(entry);
|
||||
compendiumCharacters.push(character);
|
||||
progress.advance();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
/* Migrate existing effects modifying armor, creating new Armor Effects instead */
|
||||
const migrateEffects = async entity => {
|
||||
for (const effect of entity.effects) {
|
||||
if (effect.system.changes.every(x => x.key !== 'system.armorScore')) continue;
|
||||
|
||||
effect.update({
|
||||
'system.changes': effect.system.changes.map(change => ({
|
||||
...change,
|
||||
type: change.key === 'system.armorScore' ? 'armor' : change.type,
|
||||
value: change.key === 'system.armorScore' ? { current: 0, max: change.value } : change.value
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* Migrate existing armors effects */
|
||||
const migrateItems = async items => {
|
||||
for (const item of items) {
|
||||
await migrateEffects(item);
|
||||
}
|
||||
};
|
||||
|
||||
await migrateItems([...compendiumItems, ...worldItems]);
|
||||
progress.advance({ by: progressBuffer / 2 });
|
||||
|
||||
for (const actor of [...compendiumCharacters, ...worldCharacters]) {
|
||||
await migrateEffects(actor);
|
||||
await migrateItems(actor.items);
|
||||
}
|
||||
|
||||
progress.advance({ by: progressBuffer / 2 });
|
||||
|
||||
for (let packId of lockedPacks) {
|
||||
const pack = game.packs.get(packId);
|
||||
await pack.configure({ locked: true });
|
||||
}
|
||||
|
||||
progress.close();
|
||||
|
||||
lastMigrationVersion = '2.0.0';
|
||||
}
|
||||
//#endregion
|
||||
|
||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
DhMetagamingSettings,
|
||||
DhVariantRuleSettings
|
||||
} from '../applications/settings/_module.mjs';
|
||||
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
|
||||
import { CompendiumBrowserSettings } from '../data/_module.mjs';
|
||||
|
||||
export const registerDHSettings = () => {
|
||||
registerMenuSettings();
|
||||
|
|
@ -157,12 +157,6 @@ const registerNonConfigSettings = () => {
|
|||
type: DhCountdowns
|
||||
});
|
||||
|
||||
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, {
|
||||
scope: 'world',
|
||||
config: false,
|
||||
type: DhTagTeamRoll
|
||||
});
|
||||
|
||||
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
|
||||
scope: 'world',
|
||||
config: false,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
|
|||
case socketEvent.DowntimeTrigger:
|
||||
Party.downtimeMoveQuery(data);
|
||||
break;
|
||||
case socketEvent.TagTeamStart:
|
||||
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +25,8 @@ export const socketEvent = {
|
|||
GMUpdate: 'DhGMUpdate',
|
||||
Refresh: 'DhRefresh',
|
||||
DhpFearUpdate: 'DhFearUpdate',
|
||||
DowntimeTrigger: 'DowntimeTrigger'
|
||||
DowntimeTrigger: 'DowntimeTrigger',
|
||||
TagTeamStart: 'DhTagTeamStart'
|
||||
};
|
||||
|
||||
export const GMUpdateEvent = {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
{
|
||||
"trigger": "dualityRoll",
|
||||
"triggeringActorType": "self",
|
||||
"command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n <div><div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}</div>\n <div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}</div>\n<div>${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}</div>\n <div class=\"flexrow\" style=\"gap: 8px;\">\n <button type=\"button\" id=\"hopeButton\">\n <i class=\"fa-solid fa-hands-holding\"></i>\n <label>0</label>\n </button>\n <button type=\"button\" id=\"stressButton\">\n <i class=\"fa-solid fa-bolt-lightning\"></i>\n <label>0</label>\n </button>\n </div>\n</div>`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;"
|
||||
"command": "/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n <div><div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}</div>\n <div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}</div>\n<div>${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}</div>\n <div class=\"flexrow\" style=\"gap: 8px;\">\n <button type=\"button\" id=\"hopeButton\">\n <i class=\"fa-solid fa-hands-holding\"></i>\n <label>0</label>\n </button>\n <button type=\"button\" id=\"stressButton\">\n <i class=\"fa-solid fa-bolt-lightning\"></i>\n <label>0</label>\n </button>\n </div>\n</div>`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,34 +92,28 @@
|
|||
"name": "Armorer",
|
||||
"type": "base",
|
||||
"system": {
|
||||
"rangeDependence": {
|
||||
"enabled": false,
|
||||
"type": "withinRange",
|
||||
"target": "hostile",
|
||||
"range": "melee"
|
||||
}
|
||||
},
|
||||
"_id": "cED730OjuMW5haJR",
|
||||
"img": "icons/tools/hand/hammer-and-nail.webp",
|
||||
"changes": [
|
||||
{
|
||||
"key": "system.armorScore",
|
||||
"mode": 2,
|
||||
"value": "1",
|
||||
"priority": null
|
||||
"type": "armor",
|
||||
"phase": "initial",
|
||||
"priority": 20,
|
||||
"value": {
|
||||
"max": "1",
|
||||
"interaction": "active"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
"_id": "tJw2JIPcT9hEMRXg",
|
||||
"img": "icons/tools/hand/hammer-and-nail.webp",
|
||||
"disabled": false,
|
||||
"duration": {
|
||||
"startTime": null,
|
||||
"combat": null,
|
||||
"seconds": null,
|
||||
"rounds": null,
|
||||
"turns": null,
|
||||
"startRound": null,
|
||||
"startTurn": null
|
||||
"value": null,
|
||||
"units": "seconds",
|
||||
"expiry": null,
|
||||
"expired": false
|
||||
},
|
||||
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">While you’re wearing armor, gain a +1 bonus to your </span><span class=\"tooltip-convert\" style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Armor Score</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">.</p>",
|
||||
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">While you’re wearing armor, gain a +1 bonus to your </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\" class=\"tooltip-convert\">Armor Score</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">.</span></p>",
|
||||
"origin": null,
|
||||
"tint": "#ffffff",
|
||||
"transfer": true,
|
||||
|
|
@ -129,7 +123,10 @@
|
|||
"_stats": {
|
||||
"compendiumSource": null
|
||||
},
|
||||
"_key": "!items.effects!cy8GjBPGc9w9RaGO.cED730OjuMW5haJR"
|
||||
"start": null,
|
||||
"showIcon": 1,
|
||||
"folder": null,
|
||||
"_key": "!items.effects!cy8GjBPGc9w9RaGO.tJw2JIPcT9hEMRXg"
|
||||
}
|
||||
],
|
||||
"ownership": {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,52 @@
|
|||
}
|
||||
},
|
||||
"flags": {},
|
||||
"effects": [],
|
||||
"effects": [
|
||||
{
|
||||
"name": "Bare Bones",
|
||||
"type": "base",
|
||||
"system": {
|
||||
"changes": [
|
||||
{
|
||||
"type": "armor",
|
||||
"phase": "initial",
|
||||
"priority": 20,
|
||||
"value": {
|
||||
"max": "3 + @system.traits.strength.value",
|
||||
"interaction": "inactive",
|
||||
"damageThresholds": {
|
||||
"major": "9 + (@tier - 1) * 5 + max(0, (@tier -2) * 2 )",
|
||||
"severe": "19 + (@tier - 1) * 5 + max(0, (@tier -2) * 2 )"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"_id": "FCsgz7Tdsw6QUzBs",
|
||||
"img": "icons/magic/control/buff-strength-muscle-damage-orange.webp",
|
||||
"disabled": false,
|
||||
"start": null,
|
||||
"duration": {
|
||||
"value": null,
|
||||
"units": "seconds",
|
||||
"expiry": null,
|
||||
"expired": false
|
||||
},
|
||||
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.565);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">You have a base Armor Score of 3 + your Strength.</span></p>",
|
||||
"origin": null,
|
||||
"tint": "#ffffff",
|
||||
"transfer": true,
|
||||
"statuses": [],
|
||||
"showIcon": 1,
|
||||
"folder": null,
|
||||
"sort": 0,
|
||||
"flags": {},
|
||||
"_stats": {
|
||||
"compendiumSource": null
|
||||
},
|
||||
"_key": "!items.effects!l5D9kq901JDESaXw.FCsgz7Tdsw6QUzBs"
|
||||
}
|
||||
],
|
||||
"ownership": {
|
||||
"default": 0,
|
||||
"MQSznptE5yLT7kj8": 3
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
},
|
||||
"effects": [
|
||||
{
|
||||
"_id": "LdcT1nrkd5ORCU4n",
|
||||
"_id": "ptYT10JZ2WJHvFMd",
|
||||
"onSave": false
|
||||
}
|
||||
],
|
||||
|
|
@ -252,7 +252,7 @@
|
|||
"img": "icons/magic/defensive/shield-barrier-glowing-triangle-blue.webp",
|
||||
"origin": "Compendium.daggerheart.domains.Item.YtZzYBtR0yLPPA93",
|
||||
"transfer": false,
|
||||
"_id": "LdcT1nrkd5ORCU4n",
|
||||
"_id": "ptYT10JZ2WJHvFMd",
|
||||
"type": "base",
|
||||
"system": {
|
||||
"rangeDependence": {
|
||||
|
|
@ -263,10 +263,12 @@
|
|||
},
|
||||
"changes": [
|
||||
{
|
||||
"key": "system.armorScore",
|
||||
"value": 1,
|
||||
"priority": null,
|
||||
"type": "add"
|
||||
"type": "armor",
|
||||
"phase": "initial",
|
||||
"priority": 20,
|
||||
"value": {
|
||||
"max": "1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"duration": {
|
||||
|
|
@ -298,7 +300,7 @@
|
|||
},
|
||||
"showIcon": 1,
|
||||
"folder": null,
|
||||
"_key": "!items.effects!YtZzYBtR0yLPPA93.LdcT1nrkd5ORCU4n"
|
||||
"_key": "!items.effects!YtZzYBtR0yLPPA93.ptYT10JZ2WJHvFMd"
|
||||
}
|
||||
],
|
||||
"ownership": {
|
||||
|
|
|
|||
|
|
@ -93,32 +93,25 @@
|
|||
"name": "Valor-Touched",
|
||||
"type": "base",
|
||||
"system": {
|
||||
"rangeDependence": {
|
||||
"enabled": false,
|
||||
"type": "withinRange",
|
||||
"target": "hostile",
|
||||
"range": "melee"
|
||||
}
|
||||
},
|
||||
"_id": "H9lgIqqp1imSNOv9",
|
||||
"img": "icons/magic/control/control-influence-rally-purple.webp",
|
||||
"changes": [
|
||||
{
|
||||
"key": "system.armorScore",
|
||||
"mode": 2,
|
||||
"value": "1",
|
||||
"priority": null
|
||||
"type": "armor",
|
||||
"phase": "initial",
|
||||
"priority": 20,
|
||||
"value": {
|
||||
"max": "1"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
"_id": "Ma8Zp005QYKPWIEN",
|
||||
"img": "icons/magic/control/control-influence-rally-purple.webp",
|
||||
"disabled": false,
|
||||
"duration": {
|
||||
"startTime": null,
|
||||
"combat": null,
|
||||
"seconds": null,
|
||||
"rounds": null,
|
||||
"turns": null,
|
||||
"startRound": null,
|
||||
"startTurn": null
|
||||
"value": null,
|
||||
"units": "seconds",
|
||||
"expiry": null,
|
||||
"expired": false
|
||||
},
|
||||
"description": "<ul><li class=\"vertical-card-list-found\"><p>+1 bonus to your Armor Score</p></li><li class=\"vertical-card-list-found\"><p>When you mark 1 or more Hit Points without marking an Armor Slot, clear an Armor Slot.</p></li></ul>",
|
||||
"origin": null,
|
||||
|
|
@ -130,7 +123,10 @@
|
|||
"_stats": {
|
||||
"compendiumSource": null
|
||||
},
|
||||
"_key": "!items.effects!k1AtYd3lSchIymBr.H9lgIqqp1imSNOv9"
|
||||
"start": null,
|
||||
"showIcon": 1,
|
||||
"folder": null,
|
||||
"_key": "!items.effects!k1AtYd3lSchIymBr.Ma8Zp005QYKPWIEN"
|
||||
}
|
||||
],
|
||||
"ownership": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "Item",
|
||||
"type": "Actor",
|
||||
"folder": null,
|
||||
"name": "Tier 1",
|
||||
"color": null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "Item",
|
||||
"type": "Actor",
|
||||
"folder": null,
|
||||
"name": "Tier 2",
|
||||
"color": null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "Item",
|
||||
"type": "Actor",
|
||||
"folder": null,
|
||||
"name": "Tier 3",
|
||||
"color": null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "Item",
|
||||
"type": "Actor",
|
||||
"folder": null,
|
||||
"name": "Tier 4",
|
||||
"color": null,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "LzLOJ9EVaHWAjoq9",
|
||||
"img": "icons/equipment/chest/breastplate-banded-steel-gold.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "crIbCb9NZ4K0VpoU",
|
||||
"img": "icons/equipment/chest/breastplate-layered-steel-grey.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "epkAmlZVk7HOfUUT",
|
||||
"img": "icons/equipment/chest/breastplate-purple.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "itSOp2GCyem0f7oM",
|
||||
"img": "icons/equipment/chest/breastplate-layered-leather-blue.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
{
|
||||
"folder": "tI3bfr6Sgi16Z7zm",
|
||||
"name": "Bare Bones",
|
||||
"type": "armor",
|
||||
"_id": "ITAjcigTcUw5pMCN",
|
||||
"img": "icons/magic/control/buff-strength-muscle-damage.webp",
|
||||
"system": {
|
||||
"description": "<p></p><p class=\"Body-Foundation\">When you choose not to equip armor, you have a base Armor Score of 3 + your Strength and use the following as your base damage thresholds:</p><ul><li class=\"vertical-card-list-found\"><em><strong>Tier 1:</strong></em> 9/19</li><li class=\"vertical-card-list-found\"><em><strong>Tier 2:</strong></em> 11/24</li><li class=\"vertical-card-list-found\"><em><strong>Tier 3:</strong></em> 13/31</li><li class=\"vertical-card-list-found\"><em><strong>Tier 4:</strong></em> 15/38</li></ul>",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
"tier": 1,
|
||||
"equipped": false,
|
||||
"baseScore": 3,
|
||||
"armorFeatures": [],
|
||||
"marks": {
|
||||
"value": 0
|
||||
},
|
||||
"baseThresholds": {
|
||||
"major": 9,
|
||||
"severe": 19
|
||||
}
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"name": "Bare Bones",
|
||||
"type": "base",
|
||||
"system": {
|
||||
"rangeDependence": {
|
||||
"enabled": false,
|
||||
"type": "withinRange",
|
||||
"target": "hostile",
|
||||
"range": "melee"
|
||||
}
|
||||
},
|
||||
"_id": "8ze88zUwdkQSKKJq",
|
||||
"img": "icons/magic/control/buff-strength-muscle-damage.webp",
|
||||
"changes": [
|
||||
{
|
||||
"key": "system.armorScore",
|
||||
"mode": 2,
|
||||
"value": "@system.traits.strength.value",
|
||||
"priority": 21
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"duration": {
|
||||
"startTime": null,
|
||||
"combat": null,
|
||||
"seconds": null,
|
||||
"rounds": null,
|
||||
"turns": null,
|
||||
"startRound": null,
|
||||
"startTurn": null
|
||||
},
|
||||
"description": "<p></p><p class=\"Body-Foundation\">When you choose not to equip armor, you have a base Armor Score of 3 + your Strength and use the following as your base damage thresholds:</p><ul><li class=\"vertical-card-list-found\"><em><strong>Tier 1:</strong></em> 9/19</li><li class=\"vertical-card-list-found\"><em><strong>Tier 2:</strong></em> 11/24</li><li class=\"vertical-card-list-found\"><em><strong>Tier 3:</strong></em> 13/31</li><li class=\"vertical-card-list-found\"><em><strong>Tier 4:</strong></em> 15/38</li></ul>",
|
||||
"origin": null,
|
||||
"tint": "#ffffff",
|
||||
"transfer": true,
|
||||
"statuses": [],
|
||||
"sort": 0,
|
||||
"flags": {},
|
||||
"_stats": {
|
||||
"compendiumSource": null
|
||||
},
|
||||
"_key": "!items.effects!ITAjcigTcUw5pMCN.8ze88zUwdkQSKKJq"
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"ownership": {
|
||||
"default": 0,
|
||||
"MQSznptE5yLT7kj8": 3
|
||||
},
|
||||
"flags": {},
|
||||
"_key": "!items!ITAjcigTcUw5pMCN"
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "WuoVwZA53XRAIt6d",
|
||||
"img": "icons/equipment/chest/breastplate-layered-gold.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "mNN6pvcsS10ChrWF",
|
||||
"img": "icons/equipment/chest/breastplate-collared-steel-grey.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "haULhuEg37zUUvhb",
|
||||
"img": "icons/equipment/chest/breastplate-scale-grey.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "vMJxEWz1srfwMsoj",
|
||||
"img": "icons/equipment/chest/robe-collared-blue.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "mdQ69eFHyAQUDmE7",
|
||||
"img": "icons/equipment/chest/breastplate-rivited-red.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {
|
||||
"J1MCpcfXByKaSSgx": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "hAY6UgdGT7dj22Pr",
|
||||
"img": "icons/equipment/chest/robe-layered-red.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 7
|
||||
},
|
||||
"description": "",
|
||||
"actions": {
|
||||
"8PD5JQuS05IA6HJT": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "Q6LxmtFetDDkoZVZ",
|
||||
"img": "icons/equipment/chest/breastplate-sculpted-green.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "bcQUh4QG3qFX0Vx6",
|
||||
"img": "icons/equipment/chest/breastplate-layered-gilded-orange.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {
|
||||
"L8mHf4A8SylyxsMH": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "7emTSt6nhZuTlvt5",
|
||||
"img": "icons/equipment/chest/breastplate-layered-steel.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "UdUJNa31WxFW2noa",
|
||||
"img": "icons/equipment/chest/breastplate-collared-steel.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "yJFp1bfpecDcStVK",
|
||||
"img": "icons/equipment/chest/vest-leather-tattered-white.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 3
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "dvyQeUVRLc9y6rnt",
|
||||
"img": "icons/equipment/chest/breastplate-gorget-steel.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {
|
||||
"IzM88FIxQ35P5VB2": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "K5WkjS0NGqHYmhU3",
|
||||
"img": "icons/equipment/chest/breastplate-metal-scaled-grey.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "9f7RozpPTqrzJS1m",
|
||||
"img": "icons/equipment/chest/breastplate-cuirass-steel-grey.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "jphnMZjnS2FkOH3s",
|
||||
"img": "icons/equipment/chest/breastplate-quilted-brown.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "t91M61pSCMKStTNt",
|
||||
"img": "icons/equipment/chest/breastplate-banded-simple-leather-brown.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "tzZntboNtHL5C6VM",
|
||||
"img": "icons/equipment/chest/breastplate-layered-leather-brown-silver.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 4
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "nibfdNtp2PtxvbVz",
|
||||
"img": "icons/equipment/chest/breastplate-layered-leather-brown.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 3
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "EsIN5OLKe9ZYFNXZ",
|
||||
"img": "icons/equipment/chest/breastplate-banded-blue.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 7
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "SXWjUR2aUR6bYvdl",
|
||||
"img": "icons/equipment/chest/breastplate-layered-steel-blue-gold.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 7
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "c6tMXz4rPf9ioQrf",
|
||||
"img": "icons/equipment/chest/breastplate-layered-leather-blue-gold.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "Tptgl5WOj76TyFn7",
|
||||
"img": "icons/equipment/chest/breastplate-layered-gilded-black.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "AQzU2RsqS5V5bd1v",
|
||||
"img": "icons/equipment/chest/coat-collared-red.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 6
|
||||
},
|
||||
"description": "",
|
||||
"actions": {},
|
||||
"attached": [],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"_id": "tN8kAeBvNKM3EBFo",
|
||||
"img": "icons/equipment/chest/breastplate-banded-leather-purple.webp",
|
||||
"system": {
|
||||
"armor": {
|
||||
"current": 0,
|
||||
"max": 5
|
||||
},
|
||||
"description": "",
|
||||
"actions": {
|
||||
"QRTnCYxJfuJHdnyV": {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue