Compare commits

..

No commits in common. "619315359690b7751f48c0a6edd390a414510177" and "0b5de79ca818524b90a49ff104cd302fc9a22051" have entirely different histories.

131 changed files with 1657 additions and 2025 deletions

View file

@ -43,7 +43,7 @@ CONFIG.Item.dataModels = models.items.config;
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config; CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects }; CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeTypes };
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.Combat.dataModels = { base: models.DhCombat }; CONFIG.Combat.dataModels = { base: models.DhCombat };
@ -217,6 +217,17 @@ Hooks.once('init', () => {
label: sheetLabel('DOCUMENT.ActiveEffect') label: sheetLabel('DOCUMENT.ActiveEffect')
} }
); );
DocumentSheetConfig.registerSheet(
CONFIG.ActiveEffect.documentClass,
SYSTEM.id,
applications.sheetConfigs.ArmorActiveEffectConfig,
{
types: ['armor'],
makeDefault: true,
label: () =>
`${game.i18n.localize('TYPES.ActiveEffect.armor')} ${game.i18n.localize('DAGGERHEART.GENERAL.effect')}`
}
);
game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent); game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent);
@ -270,6 +281,7 @@ Hooks.on('setup', () => {
...damageThresholds, ...damageThresholds,
'proficiency', 'proficiency',
'evasion', 'evasion',
'armorScore',
'scars', 'scars',
'levelData.level.current' 'levelData.level.current'
] ]
@ -403,17 +415,6 @@ 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 updateActorsRangeDependentEffects = async token => {
const rangeMeasurement = game.settings.get( const rangeMeasurement = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,

View file

@ -16,7 +16,8 @@
"ActiveEffect": { "ActiveEffect": {
"base": "Standard", "base": "Standard",
"beastform": "Beastform", "beastform": "Beastform",
"horde": "Horde" "horde": "Horde",
"armor": "Armor"
}, },
"Actor": { "Actor": {
"character": "Character", "character": "Character",
@ -677,35 +678,16 @@
}, },
"TagTeamSelect": { "TagTeamSelect": {
"title": "Tag Team Roll", "title": "Tag Team Roll",
"FIELDS": {
"initiator": {
"memberId": { "label": "Initiating Character" },
"cost": { "label": "Initiation Cost" }
}
},
"leaderTitle": "Initiating Character", "leaderTitle": "Initiating Character",
"membersTitle": "Participants", "membersTitle": "Participants",
"partyTeam": "Party Team", "partyTeam": "Party Team",
"hopeCost": "Hope Cost", "hopeCost": "Hope Cost",
"initiatingCharacter": "Initiating Character", "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", "linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet", "damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope", "insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create Tag Team Roll", "createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "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": { "TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions" "actorSizeUsed": "Actor size is set, determining the dimensions"
@ -796,8 +778,8 @@
}, },
"ArmorInteraction": { "ArmorInteraction": {
"none": { "label": "Ignores Armor" }, "none": { "label": "Ignores Armor" },
"active": { "label": "Active w/ Armor" }, "active": { "label": "Only Active With Armor" },
"inactive": { "label": "Inactive w/ Armor" } "inactive": { "label": "Only Active Without Armor" }
}, },
"ArmorFeature": { "ArmorFeature": {
"burning": { "burning": {
@ -1254,11 +1236,6 @@
"selectType": "Select Action Type", "selectType": "Select Action Type",
"selectAction": "Action Selection" "selectAction": "Action Selection"
}, },
"TagTeamRollTypes": {
"trait": "Trait",
"ability": "Ability",
"damageAbility": "Damage Ability"
},
"TargetTypes": { "TargetTypes": {
"any": "Any", "any": "Any",
"friendly": "Friendly", "friendly": "Friendly",
@ -1887,17 +1864,6 @@
"name": "Healing Roll" "name": "Healing Roll"
} }
}, },
"ChangeTypes": {
"armor": {
"newArmorEffect": "Armor Effect",
"FIELDS": {
"armorInteraction": {
"label": "Armor Interaction",
"hint": "Does the character wearing armor suppress this effect?"
}
}
}
},
"Duration": { "Duration": {
"passive": "Passive", "passive": "Passive",
"temporary": "Temporary" "temporary": "Temporary"
@ -1919,13 +1885,18 @@
"Attachments": { "Attachments": {
"attachHint": "Drop items here to attach them", "attachHint": "Drop items here to attach them",
"transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one." "transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one."
},
"Armor": {
"newArmorEffect": "Armor Effect",
"FIELDS": {
"armorInteraction": {
"label": "Armor Interaction",
"hint": "Does the character wearing armor suppress this effect?"
}
}
} }
}, },
"GENERAL": { "GENERAL": {
"Ability": {
"single": "Ability",
"plural": "Abilities"
},
"Action": { "Action": {
"single": "Action", "single": "Action",
"plural": "Actions" "plural": "Actions"
@ -2380,10 +2351,6 @@
"rerolled": "Rerolled", "rerolled": "Rerolled",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource", "resource": "Resource",
"result": {
"single": "Result",
"plural": "Results"
},
"roll": "Roll", "roll": "Roll",
"rollAll": "Roll All", "rollAll": "Roll All",
"rollDamage": "Roll Damage", "rollDamage": "Roll Damage",
@ -3112,7 +3079,10 @@
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token", "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}" "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"cannotAlterArmorEffectChanges": "You cannot alter the changes length of an armor effect",
"cannotAlterArmorEffectType": "You cannot alter the type of armor effect changes",
"cannotAlterArmorEffectKey": "You cannot alter they key of armor effect changes"
}, },
"Progress": { "Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry." "migrationLabel": "Performing system migration. Please wait and do not close Foundry."

View file

@ -35,6 +35,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect, toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
@ -132,6 +133,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.reactionOverride = this.reactionOverride; 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; return context;
} }
@ -208,6 +215,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static toggleSelectedEffect(_event, button) { static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render(); this.render();

View file

@ -21,12 +21,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.rulesDefault this.rulesDefault
); );
const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData); const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.type === 'armor');
const orderedArmorEffects = game.system.api.data.activeEffects.changeTypes.armor.orderEffectsForAutoChange( const orderedArmorEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange(
allArmorEffects, allArmorEffects,
true true
); );
const armor = orderedArmorEffects.reduce((acc, effect) => { const armor = orderedArmorEffects.reduce((acc, effect) => {
if (effect.type !== 'armor') return acc;
const { value, max } = effect.system.armorData; const { value, max } = effect.system.armorData;
acc.push({ acc.push({
effect: effect, effect: effect,

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -121,8 +123,16 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
return acc; return acc;
}, {}) }, {})
}; };
await this.message.update(update); 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(); await this.close();
} }

View file

@ -1,7 +1,5 @@
import { MemberData } from '../../data/tagTeamData.mjs';
import { getCritDamageBonus } from '../../helpers/utils.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -9,23 +7,15 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
constructor(party) { constructor(party) {
super(); super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.party = party; 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.tabGroups.application = Object.keys(party.system.tagTeam.members).length this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
? 'tagTeamRoll' if (refreshType === RefreshType.TagTeamRoll) {
: 'initialization'; this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.render();
Hooks.on(socketEvent.Refresh, this.tagTeamRefresh.bind()); }
});
} }
get title() { get title() {
@ -34,639 +24,324 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'TagTeamDialog',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'], classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
position: { width: 550, height: 'auto' }, position: { width: 550, height: 'auto' },
actions: { actions: {
toggleSelectMember: TagTeamDialog.#toggleSelectMember, removeMember: TagTeamDialog.#removeMember,
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll, unlinkMessage: TagTeamDialog.#unlinkMessage,
makeRoll: TagTeamDialog.#makeRoll, selectMessage: TagTeamDialog.#selectMessage,
removeRoll: TagTeamDialog.#removeRoll, createTagTeam: TagTeamDialog.#createTagTeam
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 } form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
}; };
static PARTS = { static PARTS = {
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: { application: {
tabs: [{ id: 'initialization' }, { id: 'tagTeamRoll' }] id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
} }
}; };
_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) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isEditable = this.getIsEditable(); 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
);
return context; return context;
} }
async _preparePartContext(partId, context, options) { async updateSource(update) {
const partContext = await super._preparePartContext(partId, context, options); await this.data.updateSource(update);
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);
partContext.allSelected = selectedMembers.length === 2; if (game.user.isGM) {
partContext.canStartTagTeam = partContext.allSelected && this.initiator; await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
partContext.initiator = this.initiator; Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
partContext.initiatorOptions = selectedMembers.map(x => ({ value: x.id, label: x.name })); await game.socket.emit(`system.${CONFIG.DH.id}`, {
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
});
break;
}
return partContext;
}
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, action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } data: {
}); refreshType: RefreshType.TagTeamRoll
}; }
});
await emitAsGM( } else {
GMUpdateEvent.UpdateDocument, await game.socket.emit(`system.${CONFIG.DH.id}`, {
gmUpdate, action: socketEvent.GMUpdate,
update, data: {
this.party.uuid, action: GMUpdateEvent.UpdateSetting,
options.render ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } : undefined uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
); update: this.data.toObject(),
refresh: { refreshType: RefreshType.TagTeamRoll }
} }
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);
}); });
} }
}
tagTeamRefresh = ({ refreshType, action }) => { static async updateData(_event, _element, formData) {
if (refreshType !== RefreshType.TagTeamRoll) return; 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 };
}
switch (action) { await this.updateSource(update);
case 'startTagTeamRoll':
this.tabGroups.application = 'tagTeamRoll';
break;
case 'refresh':
this.render(); 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) { static async #removeMember(_, button) {
if (initiator.cost && initiator.memberId) { const update = { [`members.${button.dataset.characterId}`]: _del };
const actor = game.actors.get(initiator.memberId); if (this.data.initiator.id === button.dataset.characterId) {
if (actor.system.resources.hope.value < initiator.cost) { 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
}
});
}
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);
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) {
return ui.notifications.warn( return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope') game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
); );
} }
} }
}
//#region Initialization const secondaryRolls = Object.keys(this.data.members)
static #toggleSelectMember(_, button) { .filter(key => key !== mainRollId)
const member = this.partyMembers.find(x => x.id === button.dataset.id); .map(key => game.messages.get(this.data.members[key].messageId));
if (member.selected && this.initiator?.memberId === member.id) this.initiator = null;
member.selected = !member.selected; const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
this.render(); const criticalRoll = systemData.roll.isCritical;
} for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
static async #startTagTeamRoll() { for (let key in roll.system.damage) {
const error = this.checkInitiatorHopeError(this.initiator); var damage = roll.system.damage[key];
if (error) return error; const damageTotal =
!roll.system.isCritical && criticalRoll
await this.party.update({ ? (await getCritDamageBonus(damage.formula)) + damage.total
'system.tagTeam': _replace( : damage.total;
new game.system.api.data.TagTeamData({ const updatedDamageParts = damage.parts;
...this.party.system.tagTeam.toObject(), if (systemData.damage[key]) {
initiator: this.initiator, if (!roll.system.isCritical && criticalRoll) {
members: this.partyMembers.reduce((acc, member) => { for (let part of updatedDamageParts) {
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); const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) { if (criticalDamage) {
part.modifierTotal += criticalDamage; damage.formula = `${damage.formula} + ${criticalDamage}`;
part.formula = `${part.formula} + ${criticalDamage}`;
part.modifierTotal = part.modifierTotal + criticalDamage;
part.total += criticalDamage; part.total += criticalDamage;
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula); part.roll = new Roll(part.formula);
} }
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
} }
} }
return newDamage; systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`;
} systemData.damage[key].total += damageTotal;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts];
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 { } else {
systemData.damage = secondaryDamage; systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts };
}
}
} }
} }
return mainRoll; systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
}
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'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: 'dualityRoll', type: 'dualityRoll',
user: game.user.id, user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }), speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
system: mainRoll.options, system: systemData,
rolls: [mainRoll], rolls: mainRoll.rolls,
sound: null, sound: null,
flags: { core: { RollTable: true } } flags: { core: { RollTable: true } }
}; };
await cls.create(msgData); 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 }; const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId in tagTeamData.members) { for (let memberId of Object.keys(this.data.members)) {
const resourceUpdates = []; const resourceUpdates = [];
const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1; const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1;
if (memberId === tagTeamData.initiator.memberId) { if (memberId === this.data.initiator.id) {
const value = tagTeamData.initiator.cost const value = this.data.initiator.cost
? rollGivesHope ? rollGivesHope
? 1 - tagTeamData.initiator.cost ? 1 - this.data.initiator.cost
: -tagTeamData.initiator.cost : -this.data.initiator.cost
: 1; : 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true }); resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
} else if (rollGivesHope) { } else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
} }
if (mainRoll.options.roll.isCritical) if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); if (systemData.roll.result.duality === -1) {
if (mainRoll.options.roll.result.duality === -1) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1; fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1; fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
} }
game.actors.get(memberId).modifyResource(resourceUpdates); this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
} }
if (fearUpdate.value) { if (fearUpdate.value) {
mainActor.modifyResource([fearUpdate]); this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
} }
/* Fin */ /* Improve by fetching default from schema */
this.cancelRoll({ confirm: false }); 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 }
}
});
}
} }
//#endregion 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);
}
} }

View file

@ -3,8 +3,10 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs'; export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-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 SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ArmorActiveEffectConfig } from './armorActiveEffectConfig.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs'; export { default as DhTokenConfig } from './token-config.mjs';
export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs'; export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs';

View file

@ -55,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
static async editEffect(event) { static async editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId; const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting( const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
this.getEffectDetails(id) this.getEffectDetails(id)
); );
if (!updatedEffect) return; if (!updatedEffect) return;

View file

@ -150,10 +150,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
minLength: 0 minLength: 0
}); });
}); });
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
} }
async _prepareContext(options) { async _prepareContext(options) {
@ -191,74 +187,38 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
break; break;
case 'changes': case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields; const fields = this.document.system.schema.fields.changes.element.fields;
const singleTypes = ['armor'];
const { base, ...typedChanges } = context.source.changes.reduce((acc, change, index) => {
const type = CONFIG.DH.GENERAL.baseActiveEffectModes[change.type] ? 'base' : change.type;
if (singleTypes.includes(type)) {
acc[type] = { ...change, index };
} else {
if (!acc[type]) acc[type] = [];
acc[type].push({ ...change, index });
}
return acc;
}, {});
partContext.changes = await Promise.all( partContext.changes = await Promise.all(
foundry.utils.deepClone(base ?? []).map(c => this._prepareChangeContext(c, fields)) foundry.utils
.deepClone(context.source.changes)
.map((c, i) => this._prepareChangeContext(c, i, fields))
); );
partContext.typedChanges = typedChanges;
break; break;
} }
return partContext; return partContext;
} }
armorChangeToggle(event) { _prepareChangeContext(change, index, fields) {
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 } } });
}
_prepareChangeContext(change, fields) {
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value); if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority; const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
Object.assign( Object.assign(
change, change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => { ['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${change.index}.${fieldName}`; paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths; return paths;
}, {}) }, {})
); );
return ( return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.( game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change, change,
change.index, index,
defaultPriority defaultPriority
) ?? ) ??
foundry.applications.handlebars.renderTemplate( foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/activeEffect/change.hbs', 'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
{ {
change, change,
index: change.index, index,
defaultPriority, defaultPriority,
fields fields
} }
@ -287,34 +247,4 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return submitData; 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 });
});
}
} }

View file

@ -0,0 +1,67 @@
const { HandlebarsApplicationMixin, DocumentSheetV2 } = foundry.applications.api;
export default class ArmorActiveEffectConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'armor-effect-config'],
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
position: { width: 560 },
actions: {
finish: ArmorActiveEffectConfig.#finish
}
};
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/armor/details.hbs' },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/armor/settings.hbs' },
footer: { template: 'systems/daggerheart/templates/sheets/activeEffect/armor/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' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemFields = context.document.system.schema.fields;
return context;
}
/** @inheritDoc */
async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context);
if (partId in partContext.tabs) partContext.tab = partContext.tabs[partId];
switch (partId) {
case 'details':
partContext.isActorEffect = this.document.parent?.documentName === 'Actor';
partContext.isItemEffect = this.document.parent?.documentName === 'Item';
break;
}
return partContext;
}
static async updateForm(_event, _form, formData) {
await this.document.update(formData.object);
this.render();
}
static #finish() {
this.close();
}
}

View file

@ -0,0 +1,223 @@
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(
' ',
'&nbsp;'
);
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 });
});
}
}

View file

@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
const effectIndex = this.move.effects.findIndex(x => x.id === id); const effectIndex = this.move.effects.findIndex(x => x.id === id);
const effect = this.move.effects[effectIndex]; const effect = this.move.effects[effectIndex];
const updatedEffect = const updatedEffect =
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect); await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return; if (!updatedEffect) return;
await this.updateMove({ await this.updateMove({

View file

@ -1,5 +1,6 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import DhDeathMove from '../../dialogs/deathMove.mjs'; import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
@ -720,16 +721,35 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Rolls an attribute check based on the clicked button's dataset attribute. * Rolls an attribute check based on the clicked button's dataset attribute.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #rollAttribute(_event, button) { static async #rollAttribute(event, button) {
const result = await this.document.rollTrait(button.dataset.attribute); 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);
if (!result) return; if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = const costResources =
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
{}; {};
result.resourceUpdates.addResources(costResources); config.resourceUpdates.addResources(costResources);
await result.resourceUpdates.updateResources(); await config.resourceUpdates.updateResources();
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method
@ -946,13 +966,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
const armorSources = []; const armorSources = [];
for (var effect of Array.from(this.document.allApplicableEffects())) { for (var effect of Array.from(this.document.allApplicableEffects())) {
const origin = effect.origin ? await foundry.utils.fromUuid(effect.origin) : effect.parent; const origin = effect.origin ? await foundry.utils.fromUuid(effect.origin) : effect.parent;
if (!effect.system.armorData || effect.disabled || effect.isSuppressed) continue; if (effect.type !== 'armor' || effect.disabled || effect.isSuppressed) continue;
const originIsActor = origin instanceof Actor;
const name = originIsActor ? effect.name : origin.name;
armorSources.push({ armorSources.push({
uuid: effect.uuid, uuid: effect.uuid,
name, name: origin.name,
...effect.system.armorData ...effect.system.armorData
}); });
} }
@ -1001,14 +1018,15 @@ export default class CharacterSheet extends DHBaseActorSheet {
/** Update specific armor source */ /** Update specific armor source */
static async armorSourceUpdate(event) { static async armorSourceUpdate(event) {
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid); const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
const armorChange = effect.system.armorChange; if (effect.system.changes.length !== 1) return;
if (!armorChange) return;
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0); const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
const newChanges = effect.system.changes.map(change => ({ const newChanges = [
...change, {
value: change.type === 'armor' ? value : change.value ...effect.system.changes[0],
})); value
}
];
event.target.value = value; event.target.value = value;
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress'); const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
@ -1020,19 +1038,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
static async armorSourcePipUpdate(event) { static async armorSourcePipUpdate(event) {
const target = event.target.closest('.armor-slot'); const target = event.target.closest('.armor-slot');
const effect = await foundry.utils.fromUuid(target.dataset.uuid); const effect = await foundry.utils.fromUuid(target.dataset.uuid);
const armorChange = effect.system.armorChange; if (effect.system.changes.length !== 1) return;
if (!armorChange) return; const { value, max } = effect.system.armorData;
const { value } = effect.system.armorData;
const inputValue = Number.parseInt(target.dataset.value); const inputValue = Number.parseInt(target.dataset.value);
const decreasing = value >= inputValue; const decreasing = value >= inputValue;
const newValue = decreasing ? inputValue - 1 : inputValue; const newValue = decreasing ? inputValue - 1 : inputValue;
const newChanges = effect.system.changes.map(change => ({ const newChanges = [
...change, {
value: change.type === 'armor' ? newValue : change.value ...effect.system.changes[0],
})); value: newValue
}
];
const container = target.closest('.slot-bar'); const container = target.closest('.slot-bar');
for (const armorSlot of container.querySelectorAll('.armor-slot i')) { for (const armorSlot of container.querySelectorAll('.armor-slot i')) {

View file

@ -35,7 +35,9 @@ export default class Party extends DHBaseActorSheet {
refeshActions: Party.#refeshActions, refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest, triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll, tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
}, },
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }] dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
}; };
@ -118,7 +120,6 @@ export default class Party extends DHBaseActorSheet {
secrets: this.document.isOwner, secrets: this.document.isOwner,
relativeTo: this.document relativeTo: this.document
}); });
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
} }
/** /**
@ -257,7 +258,11 @@ export default class Party extends DHBaseActorSheet {
} }
static async #tagTeamRoll() { static async #tagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true }); new game.system.api.applications.dialogs.TagTeamDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({
force: true
});
} }
static async #groupRoll(_params) { static async #groupRoll(_params) {

View file

@ -762,6 +762,10 @@ export default function DHApplicationMixin(Base) {
data.system.domain = parent.system.domains[0]; data.system.domain = parent.system.domains[0];
} }
if (documentClass === 'ActiveEffect') {
return cls.createDialog(data, { parent: this.document });
}
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') { if (parentIsItem && type === 'feature') {
await this.document.update({ await this.document.update({

View file

@ -64,7 +64,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
const armorEffect = this.document.system.armorEffect; const armorEffect = this.document.system.armorEffect;
if (Number.isNaN(value) || !armorEffect) return; if (Number.isNaN(value) || !armorEffect) return;
await armorEffect.system.armorChange.typeData.updateArmorMax(value); await armorEffect.system.updateArmorMax(value);
this.render(); this.render();
} }

View file

@ -190,24 +190,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const target = event.target.closest('[data-die-index]'); const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') { if (target.dataset.type === 'damage') {
const { damageType, part, dice, result } = target.dataset; game.system.api.dice.DamageRoll.reroll(target, message);
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 { } else {
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0]; let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass = const rollClass =
@ -221,16 +204,20 @@ 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 }); if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll( const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
originalRoll_parsed,
target.dataset.dieIndex,
target.dataset.type
);
await game.messages.get(message._id).update({ await game.messages.get(message._id).update({
'system.roll': newRoll, 'system.roll': newRoll,
'rolls': [parsedRoll] 'rolls': [parsedRoll]
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} }
} }

View file

@ -253,8 +253,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const item of this.items) { for (const item of this.items) {
if (['weapon', 'armor'].includes(item.type)) { if (['weapon', 'armor'].includes(item.type)) {
item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate( item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/item-tags.hbs', 'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
{ item: item.system } item.system
); );
} }
item.system.enrichedDescription = item.system.enrichedDescription =

View file

@ -959,22 +959,12 @@ export const sceneRangeMeasurementSetting = {
} }
}; };
export const tagTeamRollTypes = { export const activeEffectModes = {
trait: { armor: {
id: 'trait', id: 'armor',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait' priority: 20,
label: 'TYPES.ActiveEffect.armor'
}, },
ability: {
id: 'ability',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
},
damageAbility: {
id: 'damageAbility',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
}
};
export const baseActiveEffectModes = {
custom: { custom: {
id: 'custom', id: 'custom',
priority: 0, priority: 0,
@ -1012,15 +1002,6 @@ export const baseActiveEffectModes = {
} }
}; };
export const activeEffectModes = {
armor: {
id: 'armor',
priority: 20,
label: 'TYPES.ActiveEffect.armor'
},
...baseActiveEffectModes
};
export const activeEffectArmorInteraction = { export const activeEffectArmorInteraction = {
none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' }, none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' },
active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' }, active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' },

View file

@ -1,5 +1,4 @@
export const hooksConfig = { export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle', effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed', lockedTooltipDismissed: 'DHLockedTooltipDismissed'
tagTeamStart: 'DHTagTeamRollStart'
}; };

View file

@ -493,15 +493,19 @@ export const weaponFeatures = {
key: 'system.evasion', key: 'system.evasion',
mode: 2, mode: 2,
value: '-1' value: '-1'
}
]
}, },
{ {
key: 'Armor',
type: 'armor', type: 'armor',
typeData: { name: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
img: 'icons/skills/melee/shield-block-bash-blue.webp',
changes: [
{
type: 'armor', type: 'armor',
max: 'ITEM.@system.tier + 1' max: 'ITEM.@system.tier + 1'
} }
}
] ]
} }
] ]
@ -808,20 +812,24 @@ export const weaponFeatures = {
} }
}, },
{ {
type: 'armor',
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp', img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [ changes: [
{ {
key: 'Armor',
type: 'armor',
value: 0,
typeData: {
type: 'armor', type: 'armor',
max: 1 max: 1
} }
],
system: {
rangeDependence: {
enabled: true,
range: 'melee',
target: 'hostile',
type: 'withinRange'
}
} }
]
} }
] ]
}, },
@ -1200,19 +1208,15 @@ export const weaponFeatures = {
description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.description',
effects: [ effects: [
{ {
type: 'armor',
name: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.description',
img: 'icons/skills/melee/shield-block-gray-orange.webp', img: 'icons/skills/melee/shield-block-gray-orange.webp',
changes: [ changes: [
{ {
key: 'Armor',
type: 'armor',
value: 0,
typeData: {
type: 'armor', type: 'armor',
max: 'ITEM.@system.tier' max: 'ITEM.@system.tier'
} }
}
] ]
} }
] ]

View file

@ -34,6 +34,7 @@ export const gameSettings = {
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion', LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue', SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings' CompendiumBrowserSettings: 'CompendiumBrowserSettings'
}; };

View file

@ -1,9 +1,9 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.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 DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
export { default as TagTeamData } from './tagTeamData.mjs';
export * as countdowns from './countdowns.mjs'; export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';

View file

@ -50,8 +50,9 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) { async use(event, options) {
const result = await super.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; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id); await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
} }

View file

@ -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 * @param {Event} event Event from the button used to trigger the Action
* @returns {object} * @returns {object}
*/ */
async use(event, configOptions = {}) { async use(event) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
let config = this.prepareConfig(event, configOptions); let config = this.prepareConfig(event);
if (!config) return; if (!config) return;
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); 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 (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat(); if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
return config; 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 * @param {Event} event Event from the button used to trigger the Action
* @returns {object} * @returns {object}
*/ */
prepareBaseConfig(event, configOptions = {}) { prepareBaseConfig(event) {
const isActor = this.item instanceof CONFIG.Actor.documentClass; const isActor = this.item instanceof CONFIG.Actor.documentClass;
const actionTitle = game.i18n.localize(this.name); const actionTitle = game.i18n.localize(this.name);
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `; const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
@ -268,8 +268,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
data: this.getRollData(), data: this.getRollData(),
evaluate: this.hasRoll, evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor), resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid, targetUuid: this.targetUuid
...configOptions
}; };
DHBaseAction.applyKeybindings(config); DHBaseAction.applyKeybindings(config);
@ -281,8 +280,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {Event} event Event from the button used to trigger the Action * @param {Event} event Event from the button used to trigger the Action
* @returns {object} * @returns {object}
*/ */
prepareConfig(event, configOptions = {}) { prepareConfig(event) {
const config = this.prepareBaseConfig(event, configOptions); const config = this.prepareBaseConfig(event);
for (const clsField of Object.values(this.schema.fields)) { for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false; if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
} }
@ -357,11 +356,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
get hasDamage() { get hasDamage() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing'; return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing';
} }
get hasHealing() { get hasHealing() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing';
} }
get hasSave() { get hasSave() {

View file

@ -1,12 +1,17 @@
import BaseEffect from './baseEffect.mjs'; import BaseEffect from './baseEffect.mjs';
import BeastformEffect from './beastformEffect.mjs'; import BeastformEffect from './beastformEffect.mjs';
import HordeEffect from './hordeEffect.mjs'; import HordeEffect from './hordeEffect.mjs';
export { changeTypes, changeEffects } from './changeTypes/_module.mjs'; import ArmorEffect from './armorEffect.mjs';
export { BaseEffect, BeastformEffect, HordeEffect }; export { BaseEffect, BeastformEffect, HordeEffect, ArmorEffect };
export const config = { export const config = {
base: BaseEffect, base: BaseEffect,
beastform: BeastformEffect, beastform: BeastformEffect,
horde: HordeEffect horde: HordeEffect,
armor: ArmorEffect
};
export const changeTypes = {
armor: ArmorEffect.armorChangeEffect
}; };

View file

@ -0,0 +1,244 @@
import { getScrollTextData, itemAbleRollParse } from '../../helpers/utils.mjs';
/**
* ArmorEffects are ActiveEffects that have a static changes field of length 1. It includes current and maximum armor.
* When applied to a character, it adds to their currently marked and maximum armor.
*/
export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
changes: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({
required: true,
nullable: false,
initial: 'system.armorScore'
}),
type: new fields.StringField({
required: true,
blank: false,
initial: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
validate: ArmorEffect.#validateType
}),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField({ integer: true, initial: 20 }),
value: new fields.NumberField({
required: true,
integer: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.value'
}),
max: new fields.StringField({
required: true,
nullable: false,
initial: '1',
label: 'DAGGERHEART.GENERAL.max'
})
}),
{
initial: [
{
key: 'system.armorScore',
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
phase: 'initial',
priority: 20,
value: 0,
max: '1'
}
]
}
),
armorInteraction: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
label: 'DAGGERHEART.EFFECTS.Armor.FIELDS.armorInteraction.label',
hint: 'DAGGERHEART.EFFECTS.Armor.FIELDS.armorInteraction.hint'
})
};
}
get isSuppressed() {
if (this.parent.actor?.type !== 'character') return false;
switch (this.armorInteraction) {
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
return !this.parent.actor.system.armor;
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id:
return Boolean(this.parent.actor.system.armor);
default:
return false;
}
}
/* Type Functions */
/**
* Validate that an {@link EffectChangeData#type} string is well-formed.
* @param {string} type The string to be validated
* @returns {true}
* @throws {Error} An error if the type string is malformed
*/
static #validateType(type) {
if (type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id)
throw new Error('An armor effect must have change.type "armor"');
return true;
}
static armorChangeEffect = {
label: 'Armor',
defaultPriortiy: 20,
handler: (actor, change, _options, _field, replacementData) => {
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.armorScore.value',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
value: change.value
},
replacementData
);
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.armorScore.max',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
value: change.max
},
replacementData
);
return {};
},
render: null
};
/* Helpers */
get armorChange() {
if (this.changes.length !== 1)
throw new Error('Unexpected error. An armor effect should have a changes field of length 1.');
const actor = this.parent.actor?.type === 'character' ? this.parent.actor : null;
const changeData = this.changes[0];
const maxParse = actor ? itemAbleRollParse(changeData.max, actor, this.parent.parent) : null;
const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null;
const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null;
return {
...changeData,
max: maxEvaluated ?? changeData.max
};
}
get armorData() {
return { value: this.armorChange.value, max: this.armorChange.max };
}
async updateArmorMax(newMax) {
const { effect, ...baseChange } = this.armorChange;
const newChanges = [
{
...baseChange,
max: newMax,
value: Math.min(this.armorChange.value, newMax)
}
];
await this.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)
);
}
/* Overrides */
static getDefaultObject() {
return {
key: 'system.armorScore',
type: 'armor',
name: game.i18n.localize('DAGGERHEART.EFFECTS.Armor.newArmorEffect'),
img: 'icons/equipment/chest/breastplate-helmet-metal.webp'
};
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system?.changes) {
const changesChanged = changes.system.changes.length !== this.changes.length;
if (changesChanged) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectChanges')
);
return false;
}
if (changes.system.changes.length === 1) {
if (changes.system.changes[0].type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectType')
);
return false;
}
if (changes.system.changes[0].key !== 'system.armorScore') {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectKey')
);
return false;
}
if (
changes.system.changes[0].value !== this.armorChange.value &&
this.parent.actor?.type === 'character'
) {
options.scrollingTextData = [
getScrollTextData(this.parent.actor, changes.system.changes[0], 'armor')
];
}
}
}
}
_onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId);
if (options.scrollingTextData && this.parent.actor?.type === 'character')
this.parent.actor.queueScrollText(options.scrollingTextData);
}
}

View file

@ -12,8 +12,6 @@
* "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) * "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 { changeTypes } from './_module.mjs';
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -32,8 +30,7 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
}), }),
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' }), phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField(), priority: new fields.NumberField()
typeData: new fields.TypedSchemaField(changeTypes, { nullable: true, initial: null })
}) })
), ),
duration: new fields.SchemaField({ duration: new fields.SchemaField({
@ -89,17 +86,6 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
return true; 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.typeData.getArmorData(armorChange);
}
static getDefaultObject() { static getDefaultObject() {
return { return {
name: 'New Effect', name: 'New Effect',

View file

@ -1,9 +0,0 @@
import Armor from './armor.mjs';
export const changeEffects = {
armor: Armor.changeEffect
};
export const changeTypes = {
armor: Armor
};

View file

@ -1,147 +0,0 @@
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
const fields = foundry.data.fields;
export default class Armor extends foundry.abstract.DataModel {
static defineSchema() {
return {
type: new fields.StringField({ required: true, initial: 'armor', blank: false }),
max: new fields.StringField({
required: true,
nullable: false,
initial: '1',
label: 'DAGGERHEART.GENERAL.max'
}),
armorInteraction: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.label',
hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.hint'
})
};
}
static changeEffect = {
label: 'Armor',
defaultPriortiy: 20,
handler: (actor, change, _options, _field, replacementData) => {
const parsedMax = itemAbleRollParse(change.typeData.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
},
replacementData
);
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.armorScore.max',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
value: parsedMax
},
replacementData
);
return {};
},
render: null
};
get isSuppressed() {
switch (this.armorInteraction) {
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(locked) {
return {
key: 'Armor',
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
value: 0,
typeData: {
type: 'armor',
max: 0,
locked
},
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: [Armor.getInitialValue(true)]
}
};
}
/* Helpers */
getArmorData(parentChange) {
const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null;
const maxParse = actor ? itemAbleRollParse(this.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 {
value: parentChange.value,
max: maxEvaluated ?? this.max
};
}
async updateArmorMax(newMax) {
const newChanges = [
...this.parent.changes.map(change => ({
...change,
value: change.type === 'armor' ? Math.min(change.value, newMax) : change.value,
typeData: change.type === 'armor' ? { ...change.typeData, max: newMax } : change.typeData
}))
];
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)
);
}
}

View file

@ -189,6 +189,19 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
return true; 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) { async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId); const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return; if (allowed === false) return;

View file

@ -469,8 +469,8 @@ export default class DhCharacter extends DhCreature {
const increasing = armorChange >= 0; const increasing = armorChange >= 0;
let remainingChange = Math.abs(armorChange); let remainingChange = Math.abs(armorChange);
const armorEffects = Array.from(this.parent.allApplicableEffects()).filter(x => x.system.armorData); const armorEffects = Array.from(this.parent.allApplicableEffects()).filter(x => x.type === 'armor');
const orderedEffects = game.system.api.data.activeEffects.changeTypes.armor.orderEffectsForAutoChange( const orderedEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange(
armorEffects, armorEffects,
increasing increasing
); );
@ -482,11 +482,11 @@ export default class DhCharacter extends DhCreature {
usedArmorChange -= armorEffect.system.armorChange.value; usedArmorChange -= armorEffect.system.armorChange.value;
} else { } else {
if (increasing) { if (increasing) {
const remainingArmor = armorEffect.system.armorData.max - armorEffect.system.armorData.value; const remainingArmor = armorEffect.system.armorChange.max - armorEffect.system.armorChange.value;
usedArmorChange = Math.min(remainingChange, remainingArmor); usedArmorChange = Math.min(remainingChange, remainingArmor);
remainingChange -= usedArmorChange; remainingChange -= usedArmorChange;
} else { } else {
const changeChange = Math.min(armorEffect.system.armorData.value, remainingChange); const changeChange = Math.min(armorEffect.system.armorChange.value, remainingChange);
usedArmorChange -= changeChange; usedArmorChange -= changeChange;
remainingChange -= changeChange; remainingChange -= changeChange;
} }
@ -499,13 +499,12 @@ export default class DhCharacter extends DhCreature {
embeddedUpdates[armorEffect.parent.id].updates.push({ embeddedUpdates[armorEffect.parent.id].updates.push({
'_id': armorEffect.id, '_id': armorEffect.id,
'system.changes': armorEffect.system.changes.map(change => ({ 'system.changes': [
...change, {
value: ...armorEffect.system.armorChange,
change.type === 'armor' value: armorEffect.system.armorChange.value + usedArmorChange
? armorEffect.system.armorChange.value + usedArmorChange }
: change.value ]
}))
}); });
} }
@ -822,6 +821,7 @@ export default class DhCharacter extends DhCreature {
static migrateData(source) { static migrateData(source) {
if (typeof source.scars === 'object') source.scars = 0; 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); return super.migrateData(source);
} }

View file

@ -75,6 +75,10 @@ export default class DhEnvironment extends BaseDataActor {
); );
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => { scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll }
});
}); });
} }
} }

View file

@ -1,6 +1,5 @@
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import TagTeamData from '../tagTeamData.mjs';
export default class DhParty extends BaseDataActor { export default class DhParty extends BaseDataActor {
/**@inheritdoc */ /**@inheritdoc */
@ -15,8 +14,7 @@ export default class DhParty extends BaseDataActor {
handfuls: new fields.NumberField({ initial: 1, integer: true }), handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }), bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true }) chests: new fields.NumberField({ initial: 0, integer: true })
}), })
tagTeam: new fields.EmbeddedDataField(TagTeamData)
}; };
} }
@ -42,6 +40,23 @@ 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) { _onDelete(options, userId) {
super._onDelete(options, userId); super._onDelete(options, userId);

View file

@ -50,9 +50,9 @@ export default class DamageField extends fields.SchemaField {
formulas = DamageField.formatFormulas.call(this, formulas, config); formulas = DamageField.formatFormulas.call(this, formulas, config);
const damageConfig = { const damageConfig = {
dialog: {},
...config, ...config,
roll: formulas, roll: formulas,
dialog: {},
data: this.getRollData() data: this.getRollData()
}; };
delete damageConfig.evaluate; delete damageConfig.evaluate;

View file

@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField {
static async execute(config, targets = null, force = false) { static async execute(config, targets = null, force = false) {
if (!config.hasEffect) return; if (!config.hasEffect) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id); let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message && !config.skips.createMessage) { if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll(''); const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true; roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);

View file

@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField {
if (!config.hasSave) return; if (!config.hasSave) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id); let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message && !config.skips.createMessage) { if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll(''); const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true; roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);

View file

@ -82,24 +82,6 @@ class ResourcesField extends fields.TypedObjectField {
} }
return data; 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 }; export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };

View file

@ -52,7 +52,7 @@ export default class DHArmor extends AttachableItem {
} }
get armorEffect() { get armorEffect() {
return this.parent.effects.find(x => x.system.armorData); return this.parent.effects.find(x => x.type === 'armor');
} }
get armorData() { get armorData() {
@ -80,9 +80,9 @@ export default class DHArmor extends AttachableItem {
async _onCreate(_data, _options, userId) { async _onCreate(_data, _options, userId) {
if (userId !== game.user.id) return; if (userId !== game.user.id) return;
if (!this.parent.effects.some(x => x.system.armorData)) { if (!this.parent.effects.some(x => x.type === 'armor')) {
this.parent.createEmbeddedDocuments('ActiveEffect', [ this.parent.createEmbeddedDocuments('ActiveEffect', [
game.system.api.data.activeEffects.changeTypes.armor.getDefaultArmorEffect() game.system.api.data.activeEffects.ArmorEffect.getDefaultObject()
]); ]);
} }
} }

View file

@ -1,47 +0,0 @@
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;
}
}

View file

@ -0,0 +1,20 @@
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 })
})
)
};
}
}

View file

@ -1,5 +1,6 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs'; import { parseRallyDice } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
export default class DamageRoll extends DHRoll { export default class DamageRoll extends DHRoll {
@ -280,7 +281,10 @@ export default class DamageRoll extends DHRoll {
return mods; return mods;
} }
static async reroll(rollPart, dice, result) { static async reroll(target, message) {
const { damageType, part, dice, result } = target.dataset;
const rollPart = message.system.damage[damageType].parts[part];
let diceIndex = 0; let diceIndex = 0;
let parsedRoll = game.system.api.dice.DamageRoll.fromData({ let parsedRoll = game.system.api.dice.DamageRoll.fromData({
...rollPart.roll, ...rollPart.roll,
@ -349,6 +353,29 @@ export default class DamageRoll extends DHRoll {
}; };
}); });
return { parsedRoll, rerolledDice }; 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
}
});
} }
} }

View file

@ -21,9 +21,6 @@ export default class DHRoll extends Roll {
static async build(config = {}, message = {}) { static async build(config = {}, message = {}) {
const roll = await this.buildConfigure(config, message); const roll = await this.buildConfigure(config, message);
if (!roll) return; if (!roll) return;
if (config.skips?.createMessage) config.messageRoll = roll;
await this.buildEvaluate(roll, config, (message = {})); await this.buildEvaluate(roll, config, (message = {}));
await this.buildPost(roll, config, (message = {})); await this.buildPost(roll, config, (message = {}));
return config; return config;
@ -33,6 +30,12 @@ export default class DHRoll extends Roll {
config.hooks = [...this.getHooks(), '']; config.hooks = [...this.getHooks(), ''];
config.dialog ??= {}; 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) { for (const hook of config.hooks) {
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
} }
@ -143,7 +146,6 @@ export default class DHRoll extends Roll {
return foundry.applications.handlebars.renderTemplate(template, { return foundry.applications.handlebars.renderTemplate(template, {
...chatData, ...chatData,
parent: chatData.parent, parent: chatData.parent,
targetMode: chatData.targetMode,
metagamingSettings metagamingSettings
}); });
} }

View file

@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll {
} }
} }
static async reroll(rollBase, dieIndex, diceType) { static async reroll(rollString, target, message) {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false }); let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
const term = parsedRoll.terms[dieIndex]; const term = parsedRoll.terms[target.dataset.dieIndex];
await term.reroll(`/r1=${term.total}`); await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate(); const result = await parsedRoll.evaluate();
@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll {
options: { appearance: {} } options: { appearance: {} }
}; };
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
if (diceSoNicePresets[diceType]) { const type = target.dataset.type;
diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType]; if (diceSoNicePresets[type]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
} }
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); 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, { const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: parsedRoll.options.targets ?? [], targets: message.system.targets,
roll: { roll: {
advantage: parsedRoll.options.roll.advantage?.type, advantage: message.system.roll.advantage?.type,
difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
} }
}); });
const extraIndex = newRoll.advantage ? 3 : 2; const extraIndex = newRoll.advantage ? 3 : 2;
newRoll.extra = newRoll.extra.slice(extraIndex); newRoll.extra = newRoll.extra.slice(extraIndex);
const actor = parsedRoll.options.source.actor const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
? await foundry.utils.fromUuid(parsedRoll.options.source.actor)
: null; const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
const config = { const config = {
source: { actor: parsedRoll.options.source.actor ?? '' }, source: { actor: message.system.source.actor ?? '' },
targets: parsedRoll.targets, targets: message.system.targets,
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
roll: newRoll, roll: newRoll,
rerolledRoll: parsedRoll.roll, rerolledRoll: message.system.roll,
resourceUpdates: new ResourceUpdateMap(actor) resourceUpdates: new ResourceUpdateMap(actor)
}; };

View file

@ -78,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.'); throw new Error('The array of sub-types to restrict to must not be empty.');
} }
const creatableEffects = types || ['base']; const creatableEffects = types || ['base', 'armor'];
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => { const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
const labelKey = `TYPES.ActiveEffect.${type}`; const labelKey = `TYPES.ActiveEffect.${type}`;
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type; const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;

View file

@ -4,7 +4,6 @@ import DHFeature from '../data/item/feature.mjs';
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
import { abilities } from '../config/actorConfig.mjs';
export default class DhpActor extends Actor { export default class DhpActor extends Actor {
parties = new Set(); parties = new Set();
@ -510,30 +509,6 @@ export default class DhpActor extends Actor {
return await rollClass.build(config); 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() { get rollClass() {
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll']; return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
} }

View file

@ -177,6 +177,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); 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); 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) { async onApplyDamage(event) {

View file

@ -224,7 +224,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip(); if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip();
} }
_setAnchor(direction, options = {}) { _setAnchor(direction, options) {
const directions = this.constructor.TOOLTIP_DIRECTIONS; const directions = this.constructor.TOOLTIP_DIRECTIONS;
const pad = this.constructor.TOOLTIP_MARGIN_PX; const pad = this.constructor.TOOLTIP_MARGIN_PX;
const pos = this.element.getBoundingClientRect(); const pos = this.element.getBoundingClientRect();

View file

@ -49,8 +49,7 @@ export default class RegisterHandlebarsHelpers {
} }
static damageSymbols(damageParts) { static damageSymbols(damageParts) {
const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))]; const symbols = [...new Set(damageParts.map(x => x.type))].map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
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>`)); return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
} }

View file

@ -528,8 +528,7 @@ export function expireActiveEffects(actor, allowedTypes = null) {
export async function getCritDamageBonus(formula) { export async function getCritDamageBonus(formula) {
const critRoll = new Roll(formula); const critRoll = new Roll(formula);
await critRoll.evaluate(); return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0);
} }
export function htmlToText(html) { export function htmlToText(html) {

View file

@ -39,7 +39,6 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
'systems/daggerheart/templates/dialogs/downtime/activities.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/dialogs/dice-roll/costSelection.hbs',
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',
@ -48,7 +47,6 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
'systems/daggerheart/templates/scene/dh-config.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'
]); ]);
}; };

View file

@ -193,7 +193,7 @@ export async function runMigrations() {
} }
if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) { if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) {
const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll'); const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator);
const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => {
if (!game.actors.some(actor => actor.id === id)) { if (!game.actors.some(actor => actor.id === id)) {
@ -206,7 +206,7 @@ export async function runMigrations() {
initiator: initatorMissing ? null : tagTeam.initiator, initiator: initatorMissing ? null : tagTeam.initiator,
members: missingMembers members: missingMembers
}); });
await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
lastMigrationVersion = '1.2.7'; lastMigrationVersion = '1.2.7';
} }
@ -374,19 +374,16 @@ export async function runMigrations() {
if (migrationArmorScore !== undefined && !hasArmorEffect) { if (migrationArmorScore !== undefined && !hasArmorEffect) {
await item.createEmbeddedDocuments('ActiveEffect', [ await item.createEmbeddedDocuments('ActiveEffect', [
{ {
...game.system.api.data.activeEffects.changeTypes.armor.getDefaultArmorEffect(), ...game.system.api.data.activeEffects.ArmorEffect.getDefaultObject(),
changes: [ changes: [
{ {
key: 'Armor', key: 'system.armorScore',
type: CONFIG.DH.GENERAL.activeEffectModes.armor, type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
phase: 'initial', phase: 'initial',
priority: 20, priority: 20,
value: 0, value: 0,
typeData: {
type: 'armor',
max: migrationArmorScore.toString() max: migrationArmorScore.toString()
} }
}
] ]
} }
]); ]);

View file

@ -15,7 +15,7 @@ import {
DhMetagamingSettings, DhMetagamingSettings,
DhVariantRuleSettings DhVariantRuleSettings
} from '../applications/settings/_module.mjs'; } from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings } from '../data/_module.mjs'; import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
export const registerDHSettings = () => { export const registerDHSettings = () => {
registerMenuSettings(); registerMenuSettings();
@ -157,6 +157,12 @@ const registerNonConfigSettings = () => {
type: DhCountdowns 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, { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'world', scope: 'world',
config: false, config: false,

View file

@ -15,9 +15,6 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
case socketEvent.DowntimeTrigger: case socketEvent.DowntimeTrigger:
Party.downtimeMoveQuery(data); Party.downtimeMoveQuery(data);
break; break;
case socketEvent.TagTeamStart:
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data);
break;
} }
} }
@ -25,8 +22,7 @@ export const socketEvent = {
GMUpdate: 'DhGMUpdate', GMUpdate: 'DhGMUpdate',
Refresh: 'DhRefresh', Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate', DhpFearUpdate: 'DhFearUpdate',
DowntimeTrigger: 'DowntimeTrigger', DowntimeTrigger: 'DowntimeTrigger'
TagTeamStart: 'DhTagTeamStart'
}; };
export const GMUpdateEvent = { export const GMUpdateEvent = {

View file

@ -85,7 +85,7 @@
{ {
"trigger": "dualityRoll", "trigger": "dualityRoll",
"triggeringActorType": "self", "triggeringActorType": "self",
"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;" "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;"
} }
] ]
} }

View file

@ -90,22 +90,19 @@
"effects": [ "effects": [
{ {
"name": "Armorer", "name": "Armorer",
"type": "base", "type": "armor",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": { "max": "1"
"type": "armor", }
"max": "1", ],
"armorInteraction": "active" "armorInteraction": "active"
}
}
]
}, },
"_id": "tJw2JIPcT9hEMRXg", "_id": "tJw2JIPcT9hEMRXg",
"img": "icons/tools/hand/hammer-and-nail.webp", "img": "icons/tools/hand/hammer-and-nail.webp",

View file

@ -22,22 +22,19 @@
"effects": [ "effects": [
{ {
"name": "Bare Bones Armor", "name": "Bare Bones Armor",
"type": "base", "type": "armor",
"system": { "system": {
"changes": [ "changes": [
{ {
"value": 0, "value": 0,
"key": "Armor", "max": "3 + @system.traits.strength.value",
"key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20
"typeData": { }
"type": "armor", ],
"max": "3 + @system.traits.strength.value",
"armorInteraction": "inactive" "armorInteraction": "inactive"
}
}
]
}, },
"_id": "FCsgz7Tdsw6QUzBs", "_id": "FCsgz7Tdsw6QUzBs",
"img": "icons/magic/control/buff-strength-muscle-damage-orange.webp", "img": "icons/magic/control/buff-strength-muscle-damage-orange.webp",

View file

@ -253,7 +253,7 @@
"origin": "Compendium.daggerheart.domains.Item.YtZzYBtR0yLPPA93", "origin": "Compendium.daggerheart.domains.Item.YtZzYBtR0yLPPA93",
"transfer": false, "transfer": false,
"_id": "ptYT10JZ2WJHvFMd", "_id": "ptYT10JZ2WJHvFMd",
"type": "base", "type": "armor",
"system": { "system": {
"rangeDependence": { "rangeDependence": {
"enabled": false, "enabled": false,
@ -263,16 +263,13 @@
}, },
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "1" "max": "1"
} }
}
], ],
"duration": { "duration": {
"type": "shortRest" "type": "shortRest"

View file

@ -91,20 +91,17 @@
"effects": [ "effects": [
{ {
"name": "Valor-Touched", "name": "Valor-Touched",
"type": "base", "type": "armor",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "1" "max": "1"
} }
}
] ]
}, },
"_id": "Ma8Zp005QYKPWIEN", "_id": "Ma8Zp005QYKPWIEN",

View file

@ -1,5 +1,5 @@
{ {
"type": "Actor", "type": "Item",
"folder": null, "folder": null,
"name": "Tier 1", "name": "Tier 1",
"color": null, "color": null,

View file

@ -1,5 +1,5 @@
{ {
"type": "Actor", "type": "Item",
"folder": null, "folder": null,
"name": "Tier 2", "name": "Tier 2",
"color": null, "color": null,

View file

@ -1,5 +1,5 @@
{ {
"type": "Actor", "type": "Item",
"folder": null, "folder": null,
"name": "Tier 3", "name": "Tier 3",
"color": null, "color": null,

View file

@ -1,5 +1,5 @@
{ {
"type": "Actor", "type": "Item",
"folder": null, "folder": null,
"name": "Tier 4", "name": "Tier 4",
"color": null, "color": null,

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!LzLOJ9EVaHWAjoq9.qlzHOAnpBYzosQxK" "_key": "!items.effects!LzLOJ9EVaHWAjoq9.qlzHOAnpBYzosQxK"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "YehcKtTeJ18q0THd", "_id": "YehcKtTeJ18q0THd",

View file

@ -70,22 +70,19 @@
"_key": "!items.effects!crIbCb9NZ4K0VpoU.awdHgEaM54G3emOU" "_key": "!items.effects!crIbCb9NZ4K0VpoU.awdHgEaM54G3emOU"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "Xp0MlTLdCe2oP36X", "_id": "Xp0MlTLdCe2oP36X",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!epkAmlZVk7HOfUUT.Fq9Q93IHCchhfSss" "_key": "!items.effects!epkAmlZVk7HOfUUT.Fq9Q93IHCchhfSss"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "GyPhsm7zLznZDfN2", "_id": "GyPhsm7zLznZDfN2",

View file

@ -27,22 +27,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "XJueICAnl5vu2q2U", "_id": "XJueICAnl5vu2q2U",

View file

@ -0,0 +1,71 @@
{
"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": "armor",
"system": {
"changes": [
{
"key": "system.armorScore",
"type": "armor",
"phase": "initial",
"priority": 20,
"value": 0,
"max": "@system.traits.strength.value"
}
]
},
"_id": "C7as6q5bx3S0Xxfn",
"img": "icons/magic/control/buff-strength-muscle-damage.webp",
"disabled": false,
"duration": {
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"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
},
"start": null,
"showIcon": 1,
"folder": null,
"_key": "!items.effects!ITAjcigTcUw5pMCN.C7as6q5bx3S0Xxfn"
}
],
"sort": 0,
"ownership": {
"default": 0,
"MQSznptE5yLT7kj8": 3
},
"flags": {},
"_key": "!items!ITAjcigTcUw5pMCN"
}

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!WuoVwZA53XRAIt6d.Hy0sNtFS1JAXxgwC" "_key": "!items.effects!WuoVwZA53XRAIt6d.Hy0sNtFS1JAXxgwC"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "2OMciFns3bSETeH9", "_id": "2OMciFns3bSETeH9",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!mNN6pvcsS10ChrWF.s8KtTIngTjnOlaTP" "_key": "!items.effects!mNN6pvcsS10ChrWF.s8KtTIngTjnOlaTP"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "dIb9PWvzyS3jYDUj", "_id": "dIb9PWvzyS3jYDUj",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!haULhuEg37zUUvhb.ZfO5NjpqEIzZVlPq" "_key": "!items.effects!haULhuEg37zUUvhb.ZfO5NjpqEIzZVlPq"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "hA0EcaykFiIpg4ZH", "_id": "hA0EcaykFiIpg4ZH",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!vMJxEWz1srfwMsoj.8bwf1Ri3jYkjphEv" "_key": "!items.effects!vMJxEWz1srfwMsoj.8bwf1Ri3jYkjphEv"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "Wejd1c4e8VtnFoc4", "_id": "Wejd1c4e8VtnFoc4",

View file

@ -64,22 +64,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "FgjNYkbghYSz8gwW", "_id": "FgjNYkbghYSz8gwW",

View file

@ -90,22 +90,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "7" "max": "7"
} }
}
] ]
}, },
"_id": "n4wyEBHbHIuYNBzt", "_id": "n4wyEBHbHIuYNBzt",

View file

@ -66,22 +66,19 @@
"_key": "!items.effects!Q6LxmtFetDDkoZVZ.xGxqTCO8MjNq5Cw6" "_key": "!items.effects!Q6LxmtFetDDkoZVZ.xGxqTCO8MjNq5Cw6"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "gZfuMqjYTYLspQop", "_id": "gZfuMqjYTYLspQop",

View file

@ -88,22 +88,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "5t3jCX3AGiWBB4DN", "_id": "5t3jCX3AGiWBB4DN",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!7emTSt6nhZuTlvt5.QIefVb73cm9gYju8" "_key": "!items.effects!7emTSt6nhZuTlvt5.QIefVb73cm9gYju8"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "9jCrg3Acd75jVclW", "_id": "9jCrg3Acd75jVclW",

View file

@ -70,22 +70,19 @@
"_key": "!items.effects!UdUJNa31WxFW2noa.mfKMW9SX3Mnos1nY" "_key": "!items.effects!UdUJNa31WxFW2noa.mfKMW9SX3Mnos1nY"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "OmGtjOMcTHNN6OsH", "_id": "OmGtjOMcTHNN6OsH",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!yJFp1bfpecDcStVK.v1FNEsypRF5W6vVc" "_key": "!items.effects!yJFp1bfpecDcStVK.v1FNEsypRF5W6vVc"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "3" "max": "3"
} }
}
] ]
}, },
"_id": "ySw8mkws8rxzxsg4", "_id": "ySw8mkws8rxzxsg4",

View file

@ -81,22 +81,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "nyoNusMuukJt1MJw", "_id": "nyoNusMuukJt1MJw",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!K5WkjS0NGqHYmhU3.JHupzYULxdQzFzuj" "_key": "!items.effects!K5WkjS0NGqHYmhU3.JHupzYULxdQzFzuj"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "7FdWcilv74zKcXWk", "_id": "7FdWcilv74zKcXWk",

View file

@ -70,22 +70,19 @@
"_key": "!items.effects!9f7RozpPTqrzJS1m.wstJ1aKKtmXgCwxB" "_key": "!items.effects!9f7RozpPTqrzJS1m.wstJ1aKKtmXgCwxB"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "qgA4nbITVOp2WTpl", "_id": "qgA4nbITVOp2WTpl",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!jphnMZjnS2FkOH3s.BFwU3ErPaajUSMUz" "_key": "!items.effects!jphnMZjnS2FkOH3s.BFwU3ErPaajUSMUz"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "dtkOq7rUKj5nLGJP", "_id": "dtkOq7rUKj5nLGJP",

View file

@ -27,22 +27,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "JqnUKeUDbH4YJbVb", "_id": "JqnUKeUDbH4YJbVb",

View file

@ -77,22 +77,19 @@
"_key": "!items.effects!tzZntboNtHL5C6VM.P3aCN8PQgPXP4C9M" "_key": "!items.effects!tzZntboNtHL5C6VM.P3aCN8PQgPXP4C9M"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "6Mh24zh1c3aK60wZ", "_id": "6Mh24zh1c3aK60wZ",

View file

@ -27,22 +27,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "3" "max": "3"
} }
}
] ]
}, },
"_id": "2enPnnikOoG0oIZP", "_id": "2enPnnikOoG0oIZP",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!EsIN5OLKe9ZYFNXZ.8Oa6Y375X8UpcPph" "_key": "!items.effects!EsIN5OLKe9ZYFNXZ.8Oa6Y375X8UpcPph"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "7" "max": "7"
} }
}
] ]
}, },
"_id": "kqNdkD1d4FOQloMV", "_id": "kqNdkD1d4FOQloMV",

View file

@ -70,22 +70,19 @@
"_key": "!items.effects!SXWjUR2aUR6bYvdl.zvzkRX2Uevemmbz4" "_key": "!items.effects!SXWjUR2aUR6bYvdl.zvzkRX2Uevemmbz4"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "7" "max": "7"
} }
}
] ]
}, },
"_id": "kFxM3Et2bPzghJWm", "_id": "kFxM3Et2bPzghJWm",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!c6tMXz4rPf9ioQrf.3AUNxBoj7mp1ziJQ" "_key": "!items.effects!c6tMXz4rPf9ioQrf.3AUNxBoj7mp1ziJQ"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "jdD0dJoh8gdGdh6W", "_id": "jdD0dJoh8gdGdh6W",

View file

@ -27,22 +27,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "gP4PsefQLSTcBAQm", "_id": "gP4PsefQLSTcBAQm",

View file

@ -65,22 +65,19 @@
"_key": "!items.effects!AQzU2RsqS5V5bd1v.3n4O7PyAWMEFdr5p" "_key": "!items.effects!AQzU2RsqS5V5bd1v.3n4O7PyAWMEFdr5p"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "xMJr6Zj9zZd7v5uD", "_id": "xMJr6Zj9zZd7v5uD",

View file

@ -57,22 +57,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "ebhSsuWrFYUVkGXC", "_id": "ebhSsuWrFYUVkGXC",

View file

@ -64,22 +64,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "pw8CD3IFNqb7530v", "_id": "pw8CD3IFNqb7530v",

View file

@ -64,22 +64,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "4" "max": "4"
} }
}
] ]
}, },
"_id": "Y4ZSoO0iGxLiNr80", "_id": "Y4ZSoO0iGxLiNr80",

View file

@ -95,22 +95,19 @@
"_key": "!items.effects!8X16lJQ3xltTwynm.rkrqlwqtR9REgRx7" "_key": "!items.effects!8X16lJQ3xltTwynm.rkrqlwqtR9REgRx7"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "8" "max": "8"
} }
}
] ]
}, },
"_id": "vNGSiFtcEBCz6jFQ", "_id": "vNGSiFtcEBCz6jFQ",

View file

@ -70,22 +70,19 @@
"_key": "!items.effects!QjwsIhXKqnlvRBMv.V8CcTcVAIxHq8KNd" "_key": "!items.effects!QjwsIhXKqnlvRBMv.V8CcTcVAIxHq8KNd"
}, },
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "jfcv9NL2RtlfpECz", "_id": "jfcv9NL2RtlfpECz",

View file

@ -57,22 +57,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "5" "max": "5"
} }
}
] ]
}, },
"_id": "E8iCCJSpPbCMostx", "_id": "E8iCCJSpPbCMostx",

View file

@ -57,22 +57,19 @@
}, },
"effects": [ "effects": [
{ {
"type": "base", "type": "armor",
"name": "Armor Effect", "name": "Armor Effect",
"img": "icons/equipment/chest/breastplate-helmet-metal.webp", "img": "icons/equipment/chest/breastplate-helmet-metal.webp",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "6" "max": "6"
} }
}
] ]
}, },
"_id": "avGWSRMFFLbdJaYC", "_id": "avGWSRMFFLbdJaYC",

View file

@ -0,0 +1,12 @@
{
"type": "Item",
"folder": null,
"name": "Special",
"color": null,
"sorting": "a",
"_id": "tI3bfr6Sgi16Z7zm",
"description": "",
"sort": 0,
"flags": {},
"_key": "!folders!tI3bfr6Sgi16Z7zm"
}

View file

@ -114,20 +114,17 @@
"description": "<p>Add the item's Tier to your Armor Score</p>", "description": "<p>Add the item's Tier to your Armor Score</p>",
"img": "icons/skills/melee/shield-block-gray-orange.webp", "img": "icons/skills/melee/shield-block-gray-orange.webp",
"_id": "7285CRGdZfHCEtT2", "_id": "7285CRGdZfHCEtT2",
"type": "base", "type": "armor",
"system": { "system": {
"changes": [ "changes": [
{ {
"key": "Armor", "key": "system.armorScore",
"type": "armor", "type": "armor",
"phase": "initial", "phase": "initial",
"priority": 20, "priority": 20,
"value": 0, "value": 0,
"typeData": {
"type": "armor",
"max": "ITEM.@system.tier" "max": "ITEM.@system.tier"
} }
}
] ]
}, },
"disabled": false, "disabled": false,

Some files were not shown because too many files have changed in this diff Show more