Merged with v14-Dev

This commit is contained in:
WBHarry 2026-03-15 19:13:37 +01:00
commit 88be00567e
650 changed files with 6323 additions and 4508 deletions

View file

@ -20,7 +20,6 @@ import {
} from './module/systemRegistration/_module.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
@ -55,11 +54,13 @@ CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Canvas.layers.regions.layerClass = placeables.DhRegionLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Region.objectClass = placeables.DhRegion;
CONFIG.RollTable.documentClass = documents.DhRollTable;
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
@ -83,7 +84,6 @@ CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager();
CONFIG.debug.triggers = false;

View file

@ -14,7 +14,9 @@
"beastform": "Beastform"
},
"ActiveEffect": {
"beastform": "Beastform"
"base": "Standard",
"beastform": "Beastform",
"horde": "Horde"
},
"Actor": {
"character": "Character",
@ -703,6 +705,15 @@
}
},
"CONFIG": {
"ActiveEffectDuration": {
"temporary": "Temporary",
"act": "Next Spotlight",
"scene": "Next Scene",
"shortRest": "Next Rest",
"longRest": "Next Long Rest",
"session": "Next Session",
"custom": "Custom"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -1064,6 +1075,10 @@
"fear": "Fear",
"spotlight": "Spotlight"
},
"DaggerheartDiceAnimationEvents": {
"critical": { "name": "Critical" },
"higher": { "name": "Highest Roll" }
},
"DamageType": {
"physical": {
"name": "Physical",
@ -1238,8 +1253,8 @@
"cone": "Cone",
"emanation": "Emanation",
"inFront": "In Front",
"rect": "Rectangle",
"ray": "Ray"
"rectangle": "Rectangle",
"line": "Line"
},
"TokenSize": {
"tiny": "Tiny",
@ -2324,6 +2339,7 @@
"plurial": "Players"
},
"portrait": "Portrait",
"preview": "Preview",
"proficiency": "Proficiency",
"quantity": "Quantity",
"range": "Range",
@ -2572,6 +2588,10 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
},
"FIELDS": {
"autoExpireActiveEffects": {
"label": "Auto Expire Active Effects",
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
},
"damageReductionRulesDefault": {
"label": "Damage Reduction Rules Default",
"hint": "Wether using armor and reductions has rules on by default"
@ -2777,7 +2797,12 @@
"colorset": "Theme",
"material": "Material",
"system": "Dice Preset",
"font": "Font"
"font": "Font",
"critical": "Duality Critical Animation",
"diceAppearance": "Dice Appearance",
"animations": "Animations",
"defaultAnimations": "Set Animations As Player Defaults",
"previewAnimation": "Preview Animation"
}
},
"variantRules": {

View file

@ -554,7 +554,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
experiences: {
...this.setup.experiences,
...Object.keys(this.character.system.experiences).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[`${key}`] = _del;
return acc;
}, {})
}

View file

@ -77,8 +77,8 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
if (!this.data.optional.portrait.keep) {
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
foundry.utils.setProperty(update, 'prototypeToken.texture', _replace({}));
foundry.utils.setProperty(update, 'prototypeToken.ring', _replace({}));
}
if (this.data.optional.biography.keep)
@ -89,7 +89,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
const { system, ...rest } = update;
await this.actor.update({
...rest,
'==system': system ?? {}
system: _replace(system ?? {})
});
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -264,6 +264,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
await feature.update({ 'system.resource.value': resetValue });
}
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
this.close();
} else {
this.render();

View file

@ -245,19 +245,21 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (error) return error;
await this.party.update({
'system.==tagTeam': new game.system.api.data.TagTeamData({
...this.party.system.tagTeam.toObject(),
initiator: this.initiator,
members: this.partyMembers.reduce((acc, member) => {
if (member.selected)
acc[member.id] = {
name: member.name,
img: member.img,
rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id
};
return acc;
}, {})
})
'system.tagTeam': _replace(
new game.system.api.data.TagTeamData({
...this.party.system.tagTeam.toObject(),
initiator: this.initiator,
members: this.partyMembers.reduce((acc, member) => {
if (member.selected)
acc[member.id] = {
name: member.name,
img: member.img,
rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id
};
return acc;
}, {})
})
)
});
const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id };
@ -566,7 +568,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
return mainRoll;
}
static async #onCancelRoll(options = { confirm: true }) {
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
this.cancelRoll(options);
}
@ -584,9 +586,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
await this.updatePartyData(
{
'system.==tagTeam': {
'system.tagTeam': {
initiator: null,
members: {}
members: _replace({})
}
},
{ render: false }

View file

@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
break;
case 'summary':
const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorDamageDice = this.actor.system.attack.damage.parts.hitPoints.value.dice;
const actorRange = this.actor.system.attack.range;
let achievementExperiences = [];

View file

@ -477,7 +477,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const secondaryData = Object.keys(
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {});
await this.levelup.updateSource({
@ -511,9 +511,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
// Simple handling that doesn't cover potential Custom LevelTiers.
update[`${basePath}.-=${button.dataset.option}`] = null;
update[`${basePath}.${button.dataset.option}`] = _del;
} else {
update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
update[`${basePath}.${button.dataset.option}.${button.dataset.checkboxNr}`] = _del;
}
} else {
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {

View file

@ -62,7 +62,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Level') {
const level = await foundry.documents.Level.fromDropData(data);
if (level?.parent === this.document) return this._onSortLevel(event, level);
return;
}
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
let sceneUuid = data.uuid;
@ -114,7 +122,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
submitData.flags.daggerheart.sceneEnvironments[key] = _del;
}
}

View file

@ -12,7 +12,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
classes: ['daggerheart', 'dialog', 'dh-style', 'setting', 'appearance-settings'],
position: { width: '600', height: 'auto' },
window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
@ -70,6 +70,14 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
}
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelector('.default-animations-input')
?.addEventListener('change', this.toggleSFXOverride.bind(this));
}
/** @inheritdoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
@ -83,15 +91,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender)
if (options.isFirstRender) {
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
this.globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
}
context.setting = this.setting;
context.globalOverrides = this.globalOverrides;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
context.isGM = game.user.isGM;
return context;
}
@ -120,6 +133,9 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @protected
*/
async prepareDiceSoNiceContext(context) {
context.animationEvents = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
context.previewAnimation = this.previewAnimation;
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
@ -146,6 +162,13 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
);
context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList();
const getAnimationsOptions = key => {
const fields = context.fields.diceSoNice.fields[key].fields.sfx.fields;
return {
higher: fields.higher.fields.class.choices
};
};
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
@ -153,7 +176,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields,
animations: ['hope', 'fear'].includes(key) ? getAnimationsOptions(key) : {}
}
}),
{}
@ -169,13 +193,20 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
static async #onSubmit(_event, _form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
async toggleSFXOverride(event) {
await this.globalOverrides.diceSoNiceSFXUpdate(this.setting, event.target.checked);
this.globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
this.render();
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
@ -183,13 +214,25 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { diceSoNice, ...rest } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
const previewAnimation = rest[`${key}PreviewAnimation`];
const events = CONFIG.DH.GENERAL.daggerheartDiceAnimationEvents;
if (previewAnimation) {
if (previewAnimation === events.critical.id && diceSoNice.sfx.critical.class) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice.sfx.critical.class };
}
if (previewAnimation === events.higher.id && diceSoNice[key].sfx.higher) {
diceSoNiceRoll.dice[0].options.sfx = { specialEffect: diceSoNice[key].sfx.higher.class };
}
}
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
}

View file

@ -298,7 +298,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({
[`${path}.-=${id}`]: null
[`${path}.${id}`]: _del
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
@ -322,7 +322,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields;
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {});
@ -382,7 +382,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
[`itemFeatures.${target.dataset.type}`]: Object.keys(
this.settings.itemFeatures[target.dataset.type]
).reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {})
@ -455,12 +455,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
if (!confirmed) return;
await this.settings.updateSource({
[`domains.-=${this.selected.domain}`]: null
[`domains.${this.selected.domain}`]: _del
});
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (currentSettings.domains[this.selected.domain]) {
await currentSettings.updateSource({ [`domains.-=${this.selected.domain}`]: null });
await currentSettings.updateSource({ [`domains.${this.selected.domain}`]: _del });
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
}
@ -507,7 +507,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
static async deleteAdversaryType(_, target) {
const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
await this.settings.updateSource({ [`adversaryTypes.${key}`]: _del });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render();
@ -563,7 +563,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const { actorType, resourceKey } = target.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.-=${resourceKey}`]: null
[`resources.${actorType}.resources.${resourceKey}`]: _del
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());

View file

@ -1,3 +1,4 @@
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
@ -104,7 +105,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
static CLEAN_ARRAYS = ['cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
@ -172,6 +173,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
revealed: this.openTrigger === index
};
});
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
@ -291,18 +293,61 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
const choices = getUnusedDamageTypes(this.action.damage.parts);
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('Damage Type'),
choices,
required: true
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = { applyTo: type };
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
],
content: content,
rejectClose: false,
modal: false,
window: {
title: game.i18n.localize('Add Damage')
},
position: { width: 300 }
});
typeDialog.render(true);
}
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
const data = this.action.toObject();
const key = button.dataset.key;
delete data.damage.parts[key];
data.damage.parts[`${key}`] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}

View file

@ -18,6 +18,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
@ -173,8 +174,77 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
}
break;
case 'settings':
const groups = {
time: _loc('EFFECT.DURATION.UNITS.GROUPS.time'),
combat: _loc('EFFECT.DURATION.UNITS.GROUPS.combat')
};
partContext.durationUnits = CONST.ACTIVE_EFFECT_DURATION_UNITS.map(value => ({
value,
label: _loc(`EFFECT.DURATION.UNITS.${value}`),
group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
}));
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all(
foundry.utils
.deepClone(context.source.changes)
.map((c, i) => this._prepareChangeContext(c, i, fields))
);
break;
}
return partContext;
}
_prepareChangeContext(change, index, fields) {
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {})
);
return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change,
index,
defaultPriority
) ??
foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
{
change,
index,
defaultPriority,
fields
}
)
);
}
/** @inheritDoc */
_onChangeForm(_formConfig, event) {
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') {
const durationSection = this.element.querySelector('.custom-duration-section');
if (event.target.value === 'custom') durationSection.classList.add('visible');
else durationSection.classList.remove('visible');
const durationDescription = this.element.querySelector('.duration-description');
if (event.target.value === 'temporary') durationDescription.classList.add('visible');
else durationDescription.classList.remove('visible');
}
}
/** @inheritDoc */
_processFormData(event, form, formData) {
const submitData = super._processFormData(event, form, formData);
if (submitData.start && !submitData.start.time) submitData.start.time = '0';
else if (!submitData) submitData.start = null;
return submitData;
}
}

View file

@ -95,7 +95,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
});
if (!confirmed) return;
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
}
async _onDragStart(event) {

View file

@ -101,8 +101,8 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
if (relinkAchievementData.length > 0) {
relinkAchievementData.forEach(data => {
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] =
null;
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.${data.experience}`] =
_del;
});
} else if (relinkSelectionData.length > 0) {
relinkSelectionData.forEach(data => {
@ -137,7 +137,7 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
await this.actor.update({
...updates,
[`system.experiences.-=${target.dataset.experience}`]: null
[`system.experiences.${target.dataset.experience}`]: _del
});
}
}

View file

@ -117,6 +117,6 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
});
if (!confirmed) return;
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
}
}

View file

@ -68,9 +68,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
*/
static async #addCategory() {
await this.actor.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
'DAGGERHEART.ACTORS.Environment.newAdversary'
)
[`system.potentialAdversaries.${foundry.utils.randomID()}`]: {
label: game.i18n.localize('DAGGERHEART.ACTORS.Environment.newAdversary')
}
});
}
@ -79,7 +79,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
* @type {ApplicationClickAction}
*/
static async #removeCategory(_, target) {
await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null });
await this.actor.update({ [`system.potentialAdversaries.${target.dataset.categoryId}`]: _del });
}
/**
@ -138,4 +138,8 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
this.render();
}
}
async _onDropItem(event, item) {
console.log(item);
}
}

View file

@ -205,7 +205,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
}
});
} else {
await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del });
}
this.render();

View file

@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
changes.height = tokenSize;
}
const deletions = { '-=actorId': null, '-=actorLink': null };
const deletions = { actorId: _del, actorLink: _del };
const mergeOptions = { inplace: false, performDeletions: true };
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
this._preview.updateSource(foundry.utils.mergeObject(changes, deletions, mergeOptions));
if (this._preview?.object?.destroyed === false) {
this._preview.object.initializeSources();

View file

@ -72,19 +72,16 @@ const typeSettingsMap = {
*/
export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
* @param {DHSheetV2Configuration} [options={}]
*/
constructor(options = {}) {
super(options);
/**
* @type {foundry.applications.ux.DragDrop[]}
* @private
*/
this._dragDrop = this._createDragDropHandlers();
}
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
this._setupDragDrop();
}
/**
* The default options for the sheet.
@ -177,7 +174,9 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
/* Core dragDrop from ActorDocument is always only 1. Possible we could refactor our own */
if (Array.isArray(this._dragDrop)) this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
@ -355,14 +354,19 @@ export default function DHApplicationMixin(Base) {
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
_setupDragDrop() {
if (this._dragDrop) {
this._dragDrop.callbacks.dragStart = this._onDragStart;
this._dragDrop.callback.drop = this._onDrop;
} else {
this._dragDrop = this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
}
/**
@ -499,7 +503,10 @@ export default function DHApplicationMixin(Base) {
icon: 'fa-solid fa-explosion',
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
return (
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts)
);
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),

View file

@ -102,7 +102,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
async advantageOnRemove(event) {
await this.document.update({
[`system.advantageOn.-=${event.detail.data.value}`]: null
[`system.advantageOn.${event.detail.data.value}`]: _del
});
}
}

View file

@ -108,14 +108,15 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
getSystemFlagUpdate() {
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
(acc, formulaKey) => {
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[formulaKey] = _del;
return acc;
},
{ altFormula: {} }
);
return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
const flagData = this.daggerheartFlag.toObject();
return { ...flagData, altFormula: { ...flagData.altFormula, ...deleteUpdate.altFormula } };
}
static async #addFormula() {
@ -127,7 +128,7 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
static async #removeFormula(_event, target) {
await this.daggerheartFlag.updateSource({
[`altFormula.-=${target.dataset.key}`]: null
[`altFormula.${target.dataset.key}`]: _del
});
this.render({ internalRefresh: true });
}

View file

@ -1,52 +1,19 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
static buildTabs() {
const { settings, ...tabs } = super.TABS;
return {
...tabs,
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings
};
}
/** @override */
static TABS = {
chat: {
documentName: 'ChatMessage'
},
combat: {
documentName: 'Combat'
},
scenes: {
documentName: 'Scene',
gmOnly: true
},
actors: {
documentName: 'Actor'
},
items: {
documentName: 'Item'
},
journal: {
documentName: 'JournalEntry',
tooltip: 'SIDEBAR.TabJournal'
},
tables: {
documentName: 'RollTable'
},
cards: {
documentName: 'Cards'
},
macros: {
documentName: 'Macro'
},
playlists: {
documentName: 'Playlist'
},
compendium: {
tooltip: 'SIDEBAR.TabCompendium',
icon: 'fa-solid fa-book-atlas'
},
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings: {
tooltip: 'SIDEBAR.TabSettings',
icon: 'fa-solid fa-gears'
}
};
static TABS = DhSidebar.buildTabs();
/** @override */
static PARTS = {

View file

@ -1,4 +1,5 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { expireActiveEffects } from '../../helpers/utils.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
@ -177,6 +178,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
}
if (combatant.actor) expireActiveEffects(combatant.actor, [CONFIG.DH.GENERAL.activeEffectDurations.act.id]);
}
await this.viewed.update({

View file

@ -233,6 +233,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
this.updateSetting({ [`countdowns.${countdownId}`]: _del });
}
}

View file

@ -52,10 +52,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.countdowns');
}
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
@ -68,6 +64,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
header.querySelector('button[data-action="toggleControls"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
@ -278,10 +275,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return acc;
}, {})
};
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}

View file

@ -1,3 +1,4 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import { RefreshType } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -72,7 +73,7 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
? game.user.character
: null
: canvas.tokens.controlled[0].actor;
return actor?.getActiveEffects() ?? [];
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []);
};
toggleHidden(token, focused) {

View file

@ -251,10 +251,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
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(
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
item.system,
item.system
);
}
item.system.enrichedDescription =

View file

@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0 && x.isView;
const hasEnvironments = environments.length > 0 && x.active;
return {
...x,
hasEnvironments,
@ -39,9 +39,10 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
environments: environments
};
});
context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive);
context.scenes.viewed = context.scenes.viewed ? extendScenes([context.scenes.viewed])[0] : null;
return context;
}

View file

@ -52,46 +52,6 @@
* @extends {foundry.applications.ux.ContextMenu}
*/
export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
/**
* @param {HTMLElement|jQuery} container - The HTML element that contains the context menu targets.
* @param {string} selector - A CSS selector which activates the context menu.
* @param {ContextMenuEntry[]} menuItems - An Array of entries to display in the menu
* @param {ContextMenuOptions} [options] - Additional options to configure the context menu.
*/
constructor(container, selector, menuItems, options) {
super(container, selector, menuItems, options);
/** @deprecated since v13 until v15 */
this.#jQuery = options.jQuery;
}
/**
* Whether to pass jQuery objects or HTMLElement instances to callback.
* @type {boolean}
*/
#jQuery;
/**@inheritdoc */
activateListeners(menu) {
menu.addEventListener('click', this.#onClickItem.bind(this));
}
/**
* Handle click events on context menu items.
* @param {PointerEvent} event The click event
*/
#onClickItem(event) {
event.preventDefault();
event.stopPropagation();
const element = event.target.closest('.context-item');
if (!element) return;
const item = this.menuItems.find(i => i.element === element);
item?.callback(this.#jQuery ? $(this.target) : this.target, event);
this.close();
}
/* -------------------------------------------- */
/**
* Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event

View file

@ -1,5 +1,6 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs';
export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhRegion } from './region.mjs';
export { default as DhRegionLayer } from './regionLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -0,0 +1,12 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhRegion extends foundry.canvas.placeables.Region {
/**@inheritdoc */
_formatMeasuredDistance(distance) {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (!range.enabled) return super._formatMeasuredDistance(distance);
const { distance: resultDistance, units } = DhMeasuredTemplate.getRangeLabels(distance, range);
return `${resultDistance} ${units}`;
}
}

View file

@ -0,0 +1,98 @@
export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
const { tools, ...rest } = super.prepareSceneControls();
return {
...rest,
tools: {
select: tools.select,
templateMode: tools.templateMode,
rectangle: tools.rectangle,
circle: tools.circle,
ellipse: tools.ellipse,
cone: tools.cone,
inFront: {
name: 'inFront',
order: 7,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ring: { ...tools.ring, order: 8 },
line: { ...tools.line, order: 9 },
emanation: { ...tools.emanation, order: 10 },
polygon: { ...tools.polygon, order: 11 },
hole: { ...tools.hole, order: 12 },
snap: { ...tools.snap, order: 13 },
clear: { ...tools.clear, order: 14 }
}
};
}
/** @inheritDoc */
_isCreationToolActive() {
return this.active && (game.activeTool === 'inFront' || game.activeTool in foundry.data.BaseShapeData.TYPES);
}
_createDragShapeData(event) {
const hole = ui.controls.controls[this.options.name].tools.hole?.active ?? false;
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
const shape = super._createDragShapeData(event);
const token =
shape?.type === 'emanation' && shape.base?.type === 'token'
? this.#findTokenInBounds(event.interactionData.origin)
: null;
if (token) {
shape.base.width = token.width;
shape.base.height = token.height;
event.interactionData.origin = token.getCenterPoint();
}
return shape;
}
async placeRegion(data, options = {}) {
const preConfirm = ({ _event, document, _create, _options }) => {
const shape = document.shapes[0];
const isEmanation = shape.type === 'emanation';
if (isEmanation) {
const token = this.#findTokenInBounds(shape.base.origin);
if (!token) return options.preConfirm?.() ?? true;
const shapeData = shape.toObject();
document.updateSource({
shapes: [
{
...shapeData,
base: {
...shapeData.base,
height: token.height,
width: token.width,
x: token.x,
y: token.y
}
}
]
});
}
return options?.preConfirm?.() ?? true;
};
super.placeRegion(data, { ...options, preConfirm });
}
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
#findTokenInBounds(origin) {
const { x, y } = origin;
const gridSize = canvas.grid.size;
const inBounds = canvas.scene.tokens.filter(t => {
return x.between(t.x, t.x + t.width * gridSize) && y.between(t.y, t.y + t.height * gridSize);
});
return inBounds.length === 1 ? inBounds[0] : null;
}
}

View file

@ -1,116 +0,0 @@
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
return {
name: 'templates',
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('TEMPLATE_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
tools: {
circle: {
name: 'circle',
order: 1,
title: 'CONTROLS.MeasureCircle',
icon: 'fa-regular fa-circle',
toolclip: {
src: 'toolclips/tools/measure-circle.webm',
heading: 'CONTROLS.MeasureCircle',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
}
},
cone: {
name: 'cone',
order: 2,
title: 'CONTROLS.MeasureCone',
icon: 'fa-solid fa-angle-left',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.MeasureCone',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
inFront: {
name: 'inFront',
order: 3,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
rect: {
name: 'rect',
order: 4,
title: 'CONTROLS.MeasureRect',
icon: 'fa-regular fa-square',
toolclip: {
src: 'toolclips/tools/measure-rect.webm',
heading: 'CONTROLS.MeasureRect',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ray: {
name: 'ray',
order: 5,
title: 'CONTROLS.MeasureRay',
icon: 'fa-solid fa-up-down',
toolclip: {
src: 'toolclips/tools/measure-ray.webm',
heading: 'CONTROLS.MeasureRay',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
clear: {
name: 'clear',
order: 6,
title: 'CONTROLS.MeasureClear',
icon: 'fa-solid fa-trash',
visible: game.user.isGM,
onChange: () => canvas.templates.deleteAll(),
button: true
}
},
activeTool: 'circle'
};
}
_onDragLeftStart(event) {
const interaction = event.interactionData;
// Snap the origin to the grid
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
// Create a pending MeasuredTemplateDocument
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
const previewData = {
user: game.user.id,
t: tool,
x: interaction.origin.x,
y: interaction.origin.y,
sort: Math.max(this.getMaxSort() + 1, 0),
distance: 1,
direction: 0,
fillColor: game.user.color || '#FF0000',
hidden: event.altKey
};
const defaults = CONFIG.MeasuredTemplate.defaults;
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
else if (game.activeTool === 'inFront') previewData.angle = 180;
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
const doc = new cls(previewData, { parent: canvas.scene });
// Create a preview MeasuredTemplate object
const template = new this.constructor.placeableClass(doc);
doc._object = template;
interaction.preview = this.preview.addChild(template);
template.draw();
}
}

View file

@ -1,3 +1,4 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
@ -20,7 +21,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor?.getActiveEffects() ?? [];
const activeEffects = getIconVisibleActiveEffects(Array.from(this.actor?.allApplicableEffects() ?? []));
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects

View file

@ -70,8 +70,12 @@ export const range = {
}
};
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = {
...CONST.MEASURED_TEMPLATE_TYPES,
CIRCLE: 'circle',
CONE: 'cone',
RECTANGLE: 'rectangle',
LINE: 'line',
EMANATION: 'emanation',
INFRONT: 'inFront'
};
@ -241,8 +245,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -251,7 +255,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -275,8 +279,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -285,7 +289,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -310,8 +314,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
@ -320,7 +324,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -344,8 +348,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -354,7 +358,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -368,8 +372,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -378,7 +382,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -405,8 +409,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: healingTypes.hitPoints.id,
value: {
custom: {
@ -415,7 +419,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -439,8 +443,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: healingTypes.stress.id,
value: {
custom: {
@ -449,7 +453,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -474,8 +478,8 @@ export const defaultRestOptions = {
type: 'friendly'
},
damage: {
parts: [
{
parts: {
armor: {
applyTo: healingTypes.armor.id,
value: {
custom: {
@ -484,7 +488,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -508,8 +512,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -518,7 +522,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
},
prepareWithFriends: {
@ -532,8 +536,8 @@ export const defaultRestOptions = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hope: {
applyTo: healingTypes.hope.id,
value: {
custom: {
@ -542,7 +546,7 @@ export const defaultRestOptions = {
}
}
}
]
}
}
}
},
@ -630,7 +634,95 @@ export const diceSetNumbers = {
flat: 'Flat'
};
export const getDiceSoNicePreset = async (type, faces) => {
export const diceSoNiceSFXClasses = {
PlayAnimationBright: {
id: 'PlayAnimationBright',
label: 'DICESONICE.PlayAnimationBright'
},
PlayAnimationDark: {
id: 'PlayAnimationDark',
label: 'DICESONICE.PlayAnimationDark'
},
PlayAnimationOutline: {
id: 'PlayAnimationOutline',
label: 'DICESONICE.PlayAnimationOutline'
},
PlayAnimationImpact: {
id: 'PlayAnimationImpact',
label: 'DICESONICE.PlayAnimationImpact'
},
// PlayConfettiStrength1: {
// id: 'PlayConfettiStrength1',
// label: 'DICESONICE.PlayConfettiStrength1'
// },
// PlayConfettiStrength2: {
// id: 'PlayConfettiStrength2',
// label: 'DICESONICE.PlayConfettiStrength2'
// },
// PlayConfettiStrength3: {
// id: 'PlayConfettiStrength3',
// label: 'DICESONICE.PlayConfettiStrength3'
// },
PlayAnimationThormund: {
id: 'PlayAnimationThormund',
label: 'DICESONICE.PlayAnimationThormund'
},
PlayAnimationParticleSpiral: {
id: 'PlayAnimationParticleSpiral',
label: 'DICESONICE.PlayAnimationParticleSpiral'
},
PlayAnimationParticleSparkles: {
id: 'PlayAnimationParticleSparkles',
label: 'DICESONICE.PlayAnimationParticleSparkles'
},
PlayAnimationParticleVortex: {
id: 'PlayAnimationParticleVortex',
label: 'DICESONICE.PlayAnimationParticleVortex'
},
PlaySoundEpicWin: {
id: 'PlaySoundEpicWin',
label: 'DICESONICE.PlaySoundEpicWin'
},
PlaySoundEpicFail: {
id: 'PlaySoundEpicFail',
label: 'DICESONICE.PlaySoundEpicFail'
}
// "PlaySoundCustom",
// "PlayMacro"
};
export const daggerheartDiceAnimationEvents = {
critical: {
id: 'critical',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.critical.name'
},
higher: {
id: 'higher',
label: 'DAGGERHEART.CONFIG.DaggerheartDiceAnimationEvents.higher.name'
}
};
const getDiceSoNiceSFX = sfxOptions => {
const diceSoNice = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).diceSoNiceData;
const criticalAnimationData = diceSoNice.sfx.critical;
if (sfxOptions.critical && criticalAnimationData.class) {
return {
specialEffect: criticalAnimationData.class,
options: {}
};
}
if (sfxOptions.higher && sfxOptions.data.higher) {
return {
specialEffect: sfxOptions.data.higher.class,
options: {}
};
}
return {};
};
export const getDiceSoNicePreset = async (type, faces, sfxOptions = {}) => {
const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces);
if (!system) {
ui.notifications.error(
@ -653,16 +745,33 @@ export const getDiceSoNicePreset = async (type, faces) => {
appearance: {
...system.appearance,
...type
}
},
sfx: getDiceSoNiceSFX(sfxOptions)
};
};
export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
export const getDiceSoNicePresets = async (
result,
hopeFaces,
fearFaces,
advantageFaces = 'd6',
disadvantageFaces = 'd6'
) => {
const diceSoNice = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).diceSoNiceData;
const { isCritical, withHope, withFear } = result;
return {
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces),
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces, {
critical: isCritical,
higher: withHope,
data: diceSoNice.hope.sfx
}),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces, {
critical: isCritical,
higher: withFear,
data: diceSoNice.fear.sfx
}),
advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces),
disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces)
};
@ -864,3 +973,72 @@ export const tagTeamRollTypes = {
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
}
};
export const activeEffectModes = {
custom: {
id: 'custom',
priority: 0,
label: 'EFFECT.CHANGES.TYPES.custom'
},
multiply: {
id: 'multiply',
priority: 10,
label: 'EFFECT.CHANGES.TYPES.multiply'
},
add: {
id: 'add',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.add'
},
subtract: {
id: 'subtract',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.subtract'
},
downgrade: {
id: 'downgrade',
priority: 30,
label: 'EFFECT.CHANGES.TYPES.downgrade'
},
upgrade: {
id: 'upgrade',
priority: 40,
label: 'EFFECT.CHANGES.TYPES.upgrade'
},
override: {
id: 'override',
priority: 50,
label: 'EFFECT.CHANGES.TYPES.override'
}
};
export const activeEffectDurations = {
temporary: {
id: 'temporary',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
},
act: {
id: 'act',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.act'
},
scene: {
id: 'scene',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.scene'
},
shortRest: {
id: 'shortRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.shortRest'
},
longRest: {
id: 'longRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.longRest'
},
session: {
id: 'session',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.session'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom'
}
};

View file

@ -14,8 +14,8 @@ export const armorFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -24,7 +24,7 @@ export const armorFeatures = {
}
}
}
]
}
}
}
]
@ -732,8 +732,8 @@ export const weaponFeatures = {
type: 'hostile'
},
damage: {
parts: [
{
parts: {
stress: {
applyTo: 'stress',
value: {
custom: {
@ -742,7 +742,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
],
@ -914,8 +914,8 @@ export const weaponFeatures = {
type: 'self'
},
damage: {
parts: [
{
parts: {
hitPoints: {
applyTo: 'hitPoints',
value: {
custom: {
@ -924,7 +924,7 @@ export const weaponFeatures = {
}
}
}
]
}
}
}
]

View file

@ -72,17 +72,17 @@ const companionBaseResources = Object.freeze({
export const character = {
base: characterBaseResources,
custom: {}, // module stuff goes here
all: { ...characterBaseResources },
all: { ...characterBaseResources }
};
export const adversary = {
base: adversaryBaseResources,
custom: {}, // module stuff goes here
all: { ...adversaryBaseResources },
all: { ...adversaryBaseResources }
};
export const companion = {
base: companionBaseResources,
custom: {}, // module stuff goes here
all: { ...companionBaseResources },
all: { ...companionBaseResources }
};

View file

@ -26,6 +26,7 @@ export const gameSettings = {
Metagaming: 'Metagaming',
Homebrew: 'Homebrew',
appearance: 'Appearance',
GlobalOverrides: 'GlobalOverrides',
variantRules: 'VariantRules',
Resources: {
Fear: 'ResourcesFear'

View file

@ -26,23 +26,23 @@ export default class DHAttackAction extends DHDamageAction {
return {
value: {
multiplier: 'prof',
dice: this.item?.system?.attack.damage.parts[0].value.dice,
bonus: this.item?.system?.attack.damage.parts[0].value.bonus ?? 0
dice: this.item?.system?.attack.damage.parts.hitPoints.value.dice,
bonus: this.item?.system?.attack.damage.parts.hitPoints.value.bonus ?? 0
},
type: this.item?.system?.attack.damage.parts[0].type,
type: this.item?.system?.attack.damage.parts.hitPoints.type,
base: true
};
}
get damageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula();
}
get altDamageFormula() {
const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula();

View file

@ -355,11 +355,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
get hasDamage() {
return this.damage?.parts?.length && this.type !== 'healing';
return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing';
}
get hasHealing() {
return this.damage?.parts?.length && this.type === 'healing';
return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing';
}
get hasSave() {
@ -379,6 +379,15 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return tags;
}
static migrateData(source) {
if (source.damage?.parts && Array.isArray(source.damage.parts)) {
source.damage.parts = source.damage.parts.reduce((acc, part) => {
acc[part.applyTo] = part;
return acc;
}, {});
}
}
}
export class ResourceUpdateMap extends Map {

View file

@ -2,84 +2,4 @@ import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform'];
/* async use(event, options) {
const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations();
if (abort) return;
const calcCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(this, this.cost);
const hasCost = game.system.api.fields.ActionFields.CostField.hasCost.call(this, calcCosts);
if (!hasCost) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
return;
}
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, this.item);
if (!selected) return;
const result = await super.use(event, options);
if (!result) return;
await this.transform(selected, evolved, hybrid);
}
prepareBeastformConfig(config) {
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
const actorLevel = this.actor.system.levelData.level.current;
const actorTier =
Object.values(settingsTiers).find(
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
) ?? 1;
return {
tierLimit: this.beastform.tierAccess.exact ?? actorTier
};
}
async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
Object.keys(formCategory).forEach(advantageKey => {
advantages[advantageKey] = formCategory[advantageKey];
});
return advantages;
}, {});
formData.system.features = [
...formData.system.features,
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
}
async handleActiveTransformations() {
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
const existingEffects = beastformEffects.length > 0;
await this.actor.deleteEmbeddedDocuments(
'ActiveEffect',
beastformEffects.map(x => x.id)
);
return existingEffects;
} */
}

View file

@ -12,11 +12,35 @@
* "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)
*/
export default class BaseEffect extends foundry.abstract.TypeDataModel {
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
changes: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({ required: true }),
type: new fields.StringField({
required: true,
blank: false,
choices: CONFIG.DH.GENERAL.activeEffectModes,
initial: CONFIG.DH.GENERAL.activeEffectModes.add.id,
validate: BaseEffect.#validateType
}),
value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField()
})
),
duration: new fields.SchemaField({
type: new fields.StringField({
choices: CONFIG.DH.GENERAL.activeEffectDurations,
blank: true,
label: 'DAGGERHEART.GENERAL.type'
}),
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' })
}),
rangeDependence: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
@ -45,6 +69,23 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
};
}
/**
* 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.length < 3) throw new Error('must be at least three characters long');
if (!/^custom\.-?\d+$/.test(type) && !type.split('.').every(s => /^[a-z0-9]+$/i.test(s))) {
throw new Error(
'A change type must either be a sequence of dot-delimited, alpha-numeric substrings or of the form' +
' "custom.{number}"'
);
}
return true;
}
static getDefaultObject() {
return {
name: 'New Effect',

View file

@ -5,6 +5,7 @@ export default class BeastformEffect extends BaseEffect {
static defineSchema() {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
characterTokenData: new fields.SchemaField({
usesDynamicToken: new fields.BooleanField({ initial: false }),
tokenImg: new fields.FilePathField({
@ -99,7 +100,7 @@ export default class BeastformEffect extends BaseEffect {
token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg
}
},
'flags.daggerheart': { '-=beastformTokenImg': null, '-=beastformSubjectTexture': null }
'flags.daggerheart': { beastformTokenImg: _del, beastformSubjectTexture: _del }
};
};

View file

@ -85,14 +85,14 @@ export default class DhpAdversary extends DhCreature {
type: 'attack'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
multiplier: 'flat'
}
}
]
}
}
}
}),
@ -265,12 +265,12 @@ export default class DhpAdversary extends DhCreature {
}
// Update damage in item actions
// Parse damage, and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) {
if (!action.damage) continue;
// Parse damage, and convert all formula matches in the descriptions to the new damage
try {
const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
if (!result) continue;
for (const { previousFormula, formula } of Object.values(result)) {
const oldFormulaRegexp = new RegExp(
previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
@ -372,16 +372,14 @@ export default class DhpAdversary extends DhCreature {
/**
* Updates damage to reflect a specific value.
* @throws if damage structure is invalid for conversion
* @returns the converted formula and value as a simplified term
* @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
*/
#adjustActionDamage(action, damageMeta) {
// The current algorithm only returns a value if there is a single damage part
const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints');
if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts');
if (!action.damage?.parts.hitPoints) return null;
const result = {};
for (const property of ['value', 'valueAlt']) {
const data = hpDamageParts[0][property];
const data = action.damage.parts.hitPoints[property];
const previousFormula = data.custom.enabled
? data.custom.formula
: [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]

View file

@ -96,8 +96,8 @@ export default class DhCharacter extends DhCreature {
trait: 'strength'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
custom: {
@ -106,7 +106,7 @@ export default class DhCharacter extends DhCreature {
}
}
}
]
}
}
}
}),
@ -685,7 +685,7 @@ export default class DhCharacter extends DhCreature {
isReversed: true
};
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
}
getRollData() {
@ -721,7 +721,8 @@ export default class DhCharacter extends DhCreature {
const newHopeMax = this.system.resources.hope.max + diff;
const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value);
if (newHopeValue != this.system.resources.hope.value) {
if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } };
if (!changes.system.resources.hope) changes.system.resources.hope = { value: 0 };
changes.system.resources.hope = {
...changes.system.resources.hope,
value: changes.system.resources.hope.value + newHopeValue

View file

@ -81,15 +81,15 @@ export default class DhCompanion extends DhCreature {
bonus: 0
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
dice: 'd6',
multiplier: 'prof'
}
}
]
}
}
}
}),
@ -135,7 +135,9 @@ export default class DhCompanion extends DhCreature {
break;
case 'vicious':
if (selection.data[0] === 'damage') {
this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice);
this.attack.damage.parts.hitPoints.value.dice = adjustDice(
this.attack.damage.parts.hitPoints.value.dice
);
} else {
this.attack.range = adjustRange(this.attack.range).id;
}

View file

@ -1,7 +1,7 @@
export { ActionCollection } from './actionField.mjs';
export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs';
export { default as FormulaField } from './formulaField.mjs';
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
export { default as TriggerField } from './triggerField.mjs';
export { default as MappingField } from './mappingField.mjs';
export * as ActionFields from './action/_module.mjs';

View file

@ -1,5 +1,6 @@
import FormulaField from '../formulaField.mjs';
import { setsEqual } from '../../../helpers/utils.mjs';
import IterableTypedObjectField from '../iterableTypedObjectField.mjs';
const fields = foundry.data.fields;
@ -12,7 +13,7 @@ export default class DamageField extends fields.SchemaField {
/** @inheritDoc */
constructor(options, context = {}) {
const damageFields = {
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
parts: new IterableTypedObjectField(DHDamageData),
includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'

View file

@ -1,6 +1,5 @@
import DHActionConfig from '../../applications/sheets-configs/action-config.mjs';
import { itemAbleRollParse } from '../../helpers/utils.mjs';
import MappingField from './mappingField.mjs';
/**
* Specialized collection type for stored actions.
@ -11,9 +10,9 @@ export class ActionCollection extends Collection {
constructor(model, entries) {
super();
this.#model = model;
for (const entry of entries) {
if (!(entry instanceof game.system.api.models.actions.actionsTypes.base)) continue;
this.set(entry._id, entry);
for (const [key, value] of entries) {
if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue;
this.set(key, value);
}
}
@ -61,7 +60,7 @@ export class ActionCollection extends Collection {
/**
* Field that stores actions.
*/
export class ActionsField extends MappingField {
export class ActionsField extends foundry.data.fields.TypedObjectField {
constructor(options) {
super(new ActionField(), options);
}
@ -70,7 +69,7 @@ export class ActionsField extends MappingField {
/** @inheritDoc */
initialize(value, model, options) {
const actions = Object.values(super.initialize(value, model, options));
const actions = Object.entries(super.initialize(value, model, options));
return new ActionCollection(model, actions);
}
}
@ -88,10 +87,10 @@ export class ActionField extends foundry.data.fields.ObjectField {
/* -------------------------------------------- */
/** @override */
_cleanType(value, options) {
_cleanType(value, options, _state) {
if (!(typeof value === 'object')) value = {};
const cls = this.getModel(value);
if (cls) return cls.cleanData(value, options);
if (cls) return cls.cleanData(value, options, _state);
return value;
}
@ -111,9 +110,17 @@ export class ActionField extends foundry.data.fields.ObjectField {
* @param {object} sourceData Candidate source data of the root model.
* @param {any} fieldData The value of this field within the source data.
*/
migrateSource(sourceData, fieldData) {
const cls = this.getModel(fieldData);
if (cls) cls.migrateDataSafe(fieldData);
_migrate(sourceData, _fieldData) {
const source = sourceData ?? this.options.initial;
if (!source) return sourceData;
const cls = this.getModel(source);
if (cls) {
cls.migrateDataSafe(source);
return source;
}
return sourceData;
}
}
@ -237,11 +244,11 @@ export function ActionMixin(Base) {
: foundry.utils.getProperty(result, basePath);
}
delete() {
async delete() {
if (!this.inCollection) return this.item;
const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id);
if (!action) return this.item;
this.item.update({ [`system.${this.systemPath}.-=${this.id}`]: null });
await this.item.update({ [`system.${this.systemPath}.${this.id}`]: _del }); // Does not work. Unsure why. It worked in v13 <_<'
this.constructor._sheets.get(this.uuid)?.close();
}

View file

@ -52,8 +52,8 @@ class ResourcesField extends fields.TypedObjectField {
return key in CONFIG.DH.RESOURCE[this.actorType].all;
}
_cleanType(value, options) {
value = super._cleanType(value, options);
_cleanType(value, options, _state) {
value = super._cleanType(value, options, _state);
// If not partial, ensure all data exists
if (!options.partial) {
@ -78,10 +78,28 @@ class ResourcesField extends fields.TypedObjectField {
const resource = resources[key];
value.label = resource.label;
value.isReversed = resources[key].reverse;
value.max = typeof resource.max === 'number' ? value.max ?? resource.max : null;
value.max = typeof resource.max === 'number' ? (value.max ?? resource.max) : null;
}
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 };

View file

@ -14,7 +14,7 @@ export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.A
/** @inheritdoc */
initialize(value, model, options = {}) {
const v = super.initialize(value, model, options);
const v = super.initialize(value ?? [], model, options);
return () => {
const data = v.map(entry => (typeof entry === 'function' ? entry() : entry));
return this.options.prune ? data.filter(d => !!d) : data;

View file

@ -0,0 +1,32 @@
export default class IterableTypedObjectField extends foundry.data.fields.TypedObjectField {
constructor(model, options = { collectionClass: foundry.utils.Collection }, context = {}) {
super(new foundry.data.fields.EmbeddedDataField(model), options, context);
this.#elementClass = model;
}
#elementClass;
/** Initializes an object with an iterator. This modifies the prototype instead of */
initialize(values) {
const object = Object.create(IterableObjectPrototype);
for (const [key, value] of Object.entries(values)) {
object[key] = new this.#elementClass(value);
}
return object;
}
}
/**
* The prototype of an iterable object.
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
*/
const IterableObjectPrototype = {
[Symbol.iterator]: function* () {
for (const value of Object.values(this)) {
yield value;
}
},
map: function (func) {
return Array.from(this, func);
}
};

View file

@ -1,128 +0,0 @@
/**
* A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
*
* @param {DataField} model The class of DataField which should be embedded in this field.
* @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field.
* @property {string[]} [initialKeys] Keys that will be created if no data is provided.
* @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key.
* @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided
* by `options.initialKeys`?
*/
export default class MappingField extends foundry.data.fields.ObjectField {
constructor(model, options) {
if (!(model instanceof foundry.data.fields.DataField)) {
throw new Error('MappingField must have a DataField as its contained element');
}
super(options);
/**
* The embedded DataField definition which is contained in this field.
* @type {DataField}
*/
this.model = model;
model.parent = this;
}
/* -------------------------------------------- */
/** @inheritDoc */
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
initialKeys: null,
initialValue: null,
initialKeysOnly: false
});
}
/* -------------------------------------------- */
/** @inheritDoc */
_cleanType(value, options) {
Object.entries(value).forEach(([k, v]) => {
if (k.startsWith('-=')) return;
value[k] = this.model.clean(v, options);
});
return value;
}
/* -------------------------------------------- */
/** @inheritDoc */
getInitialValue(data) {
let keys = this.initialKeys;
const initial = super.getInitialValue(data);
if (!keys || !foundry.utils.isEmpty(initial)) return initial;
if (!(keys instanceof Array)) keys = Object.keys(keys);
for (const key of keys) initial[key] = this._getInitialValueForKey(key);
return initial;
}
/* -------------------------------------------- */
/**
* Get the initial value for the provided key.
* @param {string} key Key within the object being built.
* @param {object} [object] Any existing mapping data.
* @returns {*} Initial value based on provided field type.
*/
_getInitialValueForKey(key, object) {
const initial = this.model.getInitialValue();
return this.initialValue?.(key, initial, object) ?? initial;
}
/* -------------------------------------------- */
/** @override */
_validateType(value, options = {}) {
if (foundry.utils.getType(value) !== 'Object') throw new Error('must be an Object');
const errors = this._validateValues(value, options);
if (!foundry.utils.isEmpty(errors)) {
const failure = new foundry.data.validation.DataModelValidationFailure();
failure.elements = Object.entries(errors).map(([id, failure]) => ({ id, failure }));
throw failure.asError();
}
}
/* -------------------------------------------- */
/**
* Validate each value of the object.
* @param {object} value The object to validate.
* @param {object} options Validation options.
* @returns {Record<string, Error>} An object of value-specific errors by key.
*/
_validateValues(value, options) {
const errors = {};
for (const [k, v] of Object.entries(value)) {
if (k.startsWith('-=')) continue;
const error = this.model.validate(v, options);
if (error) errors[k] = error;
}
return errors;
}
/* -------------------------------------------- */
/** @override */
initialize(value, model, options = {}) {
if (!value) return value;
const obj = {};
const initialKeys = this.initialKeys instanceof Array ? this.initialKeys : Object.keys(this.initialKeys ?? {});
const keys = this.initialKeysOnly ? initialKeys : Object.keys(value);
for (const key of keys) {
const data = value[key] ?? this._getInitialValueForKey(key, value);
obj[key] = this.model.initialize(data, model, options);
}
return obj;
}
/* -------------------------------------------- */
/** @inheritDoc */
_getField(path) {
if (path.length === 0) return this;
else if (path.length === 1) return this.model;
path.shift();
return this.model._getField(path);
}
}

View file

@ -84,7 +84,7 @@ export default class DHArmor extends AttachableItem {
}
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
changes.system.actions = actionIds.reduce((acc, id) => {
acc[`-=${id}`] = null;
acc[id] = _del;
return acc;
}, {});

View file

@ -230,9 +230,9 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
if (changed.system?.actions) {
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
if (!changed.system.actions[key]) {
const strippedKey = key.replace('-=', '');
acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger));
const action = changed.system.actions[key];
if (action && Object.keys(action).length === 0) {
acc.push(...this.actions.get(key).triggers.map(x => x.trigger));
}
return acc;

View file

@ -101,12 +101,13 @@ export default class DHBeastform extends BaseDataItem {
const effect = this.parent.effects.find(x => x.type === 'beastform');
if (!effect) return null;
const traitBonus = effect.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
const evasionBonus = effect.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
const traitBonus =
effect.system.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0;
const evasionBonus = effect.system.changes.find(x => x.key === 'system.evasion')?.value ?? 0;
const damageDiceIndex = effect.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
const damageDiceIndex = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex');
const damageDice = damageDiceIndex ? Object.keys(CONFIG.DH.GENERAL.diceTypes)[damageDiceIndex.value] : null;
const damageBonus = effect.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
const damageBonus = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0;
return {
trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.mainTrait].label),
@ -169,17 +170,17 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
changes: [
...beastformEffect.changes,
{
key: 'system.advantageSources',
mode: 2,
value: Object.values(this.advantageOn)
.map(x => x.value)
.join(', ')
}
],
system: {
changes: [
...beastformEffect.system.changes,
{
key: 'system.advantageSources',
mode: 2,
value: Object.values(this.advantageOn)
.map(x => x.value)
.join(', ')
}
],
characterTokenData: {
usesDynamicToken: this.parent.parent.prototypeToken.ring.enabled,
tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -52,6 +52,9 @@ export default class DHSubclass extends BaseDataItem {
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
if (this.actor?.type === 'character') {
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`;
if (this.actor.system.class.subclass) {
@ -86,9 +89,6 @@ export default class DHSubclass extends BaseDataItem {
}
}
}
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
}
/**@inheritdoc */

View file

@ -63,15 +63,15 @@ export default class DHWeapon extends AttachableItem {
type: 'attack'
},
damage: {
parts: [
{
parts: {
hitPoints: {
type: ['physical'],
value: {
multiplier: 'prof',
dice: 'd8'
}
}
]
}
}
}
}),
@ -148,7 +148,7 @@ export default class DHWeapon extends AttachableItem {
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
changes.system.actions = removedActionsUpdate.reduce((acc, id) => {
acc[`-=${id}`] = null;
acc[id] = _del;
return acc;
}, {});

View file

@ -1,6 +1,16 @@
export default class DhAppearance extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
static sfxSchema = () =>
new foundry.data.fields.SchemaField({
class: new foundry.data.fields.StringField({
nullable: true,
initial: null,
blank: true,
choices: CONFIG.DH.GENERAL.diceSoNiceSFXClasses
})
});
static defineSchema() {
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
@ -15,7 +25,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
material: new StringField({ initial: 'metal', required: true, blank: false }),
system: new StringField({ initial: 'standard', required: true, blank: false }),
font: new StringField({ initial: 'auto', required: true, blank: false })
font: new StringField({ initial: 'auto', required: true, blank: false }),
sfx: new SchemaField({
higher: DhAppearance.sfxSchema()
})
});
return {
@ -30,7 +43,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' }),
sfx: new SchemaField({
critical: DhAppearance.sfxSchema()
})
}),
extendCharacterDescriptions: new BooleanField(),
extendAdversaryDescriptions: new BooleanField(),
@ -65,4 +81,48 @@ export default class DhAppearance extends foundry.abstract.DataModel {
showGenericStatusEffects: new BooleanField({ initial: true })
};
}
get diceSoNiceData() {
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
const getSFX = (baseClientData, overrideKey) => {
if (!globalOverrides.diceSoNice.sfx.overrideEnabled) return baseClientData;
const overrideData = globalOverrides.diceSoNice.sfx[overrideKey];
const clientData = foundry.utils.deepClone(baseClientData);
return Object.keys(clientData).reduce((acc, key) => {
const data = clientData[key];
acc[key] = Object.keys(data).reduce((acc, dataKey) => {
const value = data[dataKey];
acc[dataKey] = value ? value : overrideData[key][dataKey];
return acc;
}, {});
return acc;
}, {});
};
return {
...this.diceSoNice,
sfx: getSFX(this.diceSoNice.sfx, 'global'),
hope: {
...this.diceSoNice.hope,
sfx: getSFX(this.diceSoNice.hope.sfx, 'hope')
},
fear: {
...this.diceSoNice.fear,
sfx: getSFX(this.diceSoNice.fear.sfx, 'fear')
}
};
}
/** Invoked by the setting when data changes */
handleChange() {
if (this.displayFear) {
if (ui.resources) {
if (this.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
const globalOverrides = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides);
globalOverrides.diceSoNiceSFXUpdate(this);
}
}

View file

@ -196,6 +196,11 @@ export default class DhAutomation extends foundry.abstract.DataModel {
})
})
}),
autoExpireActiveEffects: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.autoExpireActiveEffects.label'
}),
triggers: new fields.SchemaField({
enabled: new fields.BooleanField({
nullable: false,

View file

@ -0,0 +1,55 @@
import DhAppearance from './Appearance.mjs';
/**
* A setting to handle cases where we want to allow the GM to set a global default for client settings.
*/
export default class DhGlobalOverrides extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
diceSoNice: new fields.SchemaField({
sfx: new fields.SchemaField({
overrideEnabled: new fields.BooleanField(),
global: new fields.SchemaField({
critical: DhAppearance.sfxSchema()
}),
hope: new fields.SchemaField({
higher: DhAppearance.sfxSchema()
}),
fear: new fields.SchemaField({
higher: DhAppearance.sfxSchema()
})
})
})
};
}
async diceSoNiceSFXUpdate(appearanceSettings, enabled) {
if (!game.user.isGM) return;
const newEnabled = enabled !== undefined ? enabled : this.diceSoNice.sfx.overrideEnabled;
if (newEnabled) {
const newOverrides = foundry.utils.mergeObject(this.toObject(), {
diceSoNice: {
sfx: {
overrideEnabled: true,
global: appearanceSettings.diceSoNice.sfx,
hope: appearanceSettings.diceSoNice.hope.sfx,
fear: appearanceSettings.diceSoNice.fear.sfx
}
}
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
} else {
const newOverrides = {
...this.toObject(),
diceSoNice: {
sfx: {
overrideEnabled: false
}
}
};
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, newOverrides);
}
}
}

View file

@ -220,7 +220,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
return result;
}, {}),
...config.custom,
...config.base,
...config.base
});
}
}

View file

@ -3,3 +3,4 @@ export { default as DhAutomation } from './Automation.mjs';
export { default as DhHomebrew } from './Homebrew.mjs';
export { default as DhMetagaming } from './Metagaming.mjs';
export { default as DhVariantRules } from './VariantRules.mjs';
export { default as DhGlobalOverrides } from './GlobalOverrides.mjs';

View file

@ -197,7 +197,7 @@ export default class DamageRoll extends DHRoll {
if (config.data.parent.appliedEffects) {
// Bardic Rally
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a;
}, []);

View file

@ -140,7 +140,11 @@ export default class DHRoll extends Roll {
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, { ...chatData, metagamingSettings });
return foundry.applications.handlebars.renderTemplate(template, {
...chatData,
parent: chatData.parent,
metagamingSettings
});
}
/** @inheritDoc */
@ -261,12 +265,12 @@ export default class DHRoll extends Roll {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
if (effect.system.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
changes: effect.system.changes,
origEffect: effect,
selected: !effect.disabled
};

View file

@ -67,7 +67,7 @@ export default class DualityRoll extends D20Roll {
setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a;
}, []);
@ -179,7 +179,7 @@ export default class DualityRoll extends D20Roll {
static async buildConfigure(config = {}, message = {}) {
config.dialog ??= {};
config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
const change = c.system.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical');
if (change) a = true;
return a;
}, false);
@ -378,6 +378,8 @@ export default class DualityRoll extends D20Roll {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false });
const term = parsedRoll.terms[dieIndex];
await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
@ -401,8 +403,6 @@ export default class DualityRoll extends D20Roll {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
await parsedRoll.evaluate();
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: parsedRoll.options.targets ?? [],
roll: {

View file

@ -8,5 +8,4 @@ export { default as DhRollTable } from './rollTable.mjs';
export { default as DhScene } from './scene.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';
export { default as DhTemplateManager } from './templateManager.mjs';
export { default as DhTokenManager } from './tokenManager.mjs';

View file

@ -50,10 +50,55 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
});
}
/**
* Whether this Active Effect is eligible to be registered with the {@link ActiveEffectRegistry}
*/
get isExpiryTrackable() {
return (
this.persisted &&
!this.inCompendium &&
this.modifiesActor &&
this.start &&
this.isTemporary &&
!this.isExpired
);
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
static async createDialog(data = {}, createOptions = {}, options = {}) {
const { folders, types, template, context = {}, ...dialogOptions } = options;
if (types?.length === 0) {
throw new Error('The array of sub-types to restrict to must not be empty.');
}
const creatableEffects = ['base'];
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
const labelKey = `TYPES.ActiveEffect.${type}`;
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;
return { value: type, label };
});
if (!documentTypes.length) {
throw new Error('No document types were permitted to be created.');
}
const sortedTypes = documentTypes.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
return await super.createDialog(data, createOptions, {
folders,
types,
template,
context: { types: sortedTypes, ...context },
...dialogOptions
});
}
/**@inheritdoc*/
async _preCreate(data, options, user) {
const update = {};
@ -109,9 +154,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/* -------------------------------------------- */
/**@inheritdoc*/
static applyField(model, change, field) {
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyField(model, change, field);
static applyChangeField(model, change, field) {
change.value = Number.isNumeric(change.value)
? change.value
: DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyChangeField(model, change, field);
}
_applyLegacy(actor, change, changes) {
@ -119,13 +166,12 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
super._applyLegacy(actor, change, changes);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
let key = change.value.toString();
const isOriginTarget = key.toLowerCase().includes('origin.@');
let parseModel = model;
if (isOriginTarget && effect.origin) {
value = change.value.replaceAll(/origin\.@/gi, '@');
key = change.key.replaceAll(/origin\.@/gi, '@');
try {
const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc =
@ -136,8 +182,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
return evalValue ?? value;
const evalValue = this.effectSafeEval(itemAbleRollParse(key, parseModel, effect.parent));
return evalValue ?? key;
}
/**

View file

@ -143,7 +143,7 @@ export default class DhpActor extends Actor {
}
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
if (Number(level) > usedLevel) acc[`-=${level}`] = null;
if (Number(level) > usedLevel) acc[level] = _del;
return acc;
}, {});
@ -188,7 +188,7 @@ export default class DhpActor extends Actor {
if (experiences.length > 0) {
const getUpdate = () => ({
'system.experiences': experiences.reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {})
});

View file

@ -1,105 +0,0 @@
/**
* A singleton class that handles preview templates.
*/
export default class DhTemplateManager {
#activePreview;
/**
* Create a template preview, deactivating any existing ones.
* @param {object} data
*/
async createPreview(data) {
const template = await canvas.templates._createPreview(data, { renderSheet: false });
this.#activePreview = {
document: template.document,
object: template,
origin: { x: template.document.x, y: template.document.y }
};
this.#activePreview.events = {
contextmenu: this.#cancelTemplate.bind(this),
mousedown: this.#confirmTemplate.bind(this),
mousemove: this.#onDragMouseMove.bind(this),
wheel: this.#onMouseWheel.bind(this)
};
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
canvas.app.view.addEventListener('wheel', this.#activePreview.events.wheel, true);
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
}
/**
* Handles the movement of the temlate preview on mousedrag.
* @param {mousemove Event} event
*/
#onDragMouseMove(event) {
event.stopPropagation();
const { moveTime, object } = this.#activePreview;
const update = {};
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
let cursor = event.getLocalPosition(canvas.templates);
Object.assign(update, canvas.grid.getCenterPoint(cursor));
object.document.updateSource(update);
object.renderFlags.set({ refresh: true });
}
/**
* Handles the rotation of the preview template on scrolling.
* @param {wheel Event} event
*/
#onMouseWheel(event) {
if (!this.#activePreview) {
return;
}
if (!event.shiftKey && !event.ctrlKey) return;
event.stopPropagation();
event.preventDefault();
const { moveTime, object } = this.#activePreview;
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
const multiplier = event.shiftKey ? 0.2 : 0.1;
object.document.updateSource({
direction: object.document.direction + event.deltaY * multiplier
});
object.renderFlags.set({ refresh: true });
}
/**
* Cancels the preview template on right-click.
* @param {contextmenu Event} event
*/
#cancelTemplate(event) {
const { mousemove, mousedown, contextmenu, wheel } = this.#activePreview.events;
canvas.templates._onDragLeftCancel(event);
canvas.stage.off('mousemove', mousemove);
canvas.stage.off('mousedown', mousedown);
canvas.app.view.removeEventListener('contextmenu', contextmenu);
canvas.app.view.removeEventListener('wheel', wheel);
}
/**
* Creates a real MeasuredTemplate at the preview location and cancels the preview.
* @param {click Event} event
*/
#confirmTemplate(event) {
event.stopPropagation();
this.#cancelTemplate(event);
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]);
this.#activePreview = undefined;
}
}

View file

@ -494,4 +494,62 @@ export default class DHToken extends CONFIG.Token.documentClass {
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
}
}
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
_onRelatedUpdate(update = {}, operation = {}) {
this.#refreshOverrides(operation);
this._prepareBars();
// Update tracked Combat resource
const combatant = this.combatant;
if (combatant) {
const isActorUpdate = [this, null, undefined].includes(operation.parent);
const resource = game.combat.settings.resource;
const updates = Array.isArray(update) ? update : [update];
if (isActorUpdate && resource && updates.some(u => foundry.utils.hasProperty(u.system ?? {}, resource))) {
combatant.updateResource();
}
ui.combat.render();
}
// Trigger redraws on the token
if (this.parent.isView) {
if (this.object?.hasActiveHUD) canvas.tokens.hud.render();
this.object?.renderFlags.set({ redrawEffects: true });
for (const key of ['bar1', 'bar2']) {
const name = `${this.object?.objectId}.animate${key.capitalize()}`;
const easing = foundry.canvas.animation.CanvasAnimation.easeInOutCosine;
this.object?.animate({ [key]: this[key] }, { name, easing });
}
for (const app of foundry.applications.sheets.TokenConfig.instances()) {
app._preview?.updateSource({ delta: this.toObject().delta }, { diff: false, recursive: false });
app._preview?.object?.renderFlags.set({ refreshBars: true, redrawEffects: true });
}
}
}
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
#refreshOverrides(operation) {
if (!this.actor) return;
const { deepClone, mergeObject, equals, isEmpty } = foundry.utils;
const oldOverrides = deepClone(this._overrides) ?? {};
const newOverrides = deepClone(this.actor?.tokenOverrides ?? {}, { prune: true });
if (!equals(oldOverrides, newOverrides)) {
this._overrides = newOverrides;
this.reset();
// Send emulated update data to the PlaceableObject
if (!canvas.ready || canvas.scene !== this.scene) return;
const { width, height, depth, ...changes } = mergeObject(
mergeObject(oldOverrides, this, { insertKeys: false, insertValues: false }),
this._overrides
);
this.object?._onUpdate(changes, {}, game.user.id);
// Hand off size changes to a secondary handler requiring downstream implementation.
const sizeChanges = deepClone({ width, height, depth }, { prune: true });
if (!isEmpty(sizeChanges)) this._onOverrideSize(sizeChanges, operation);
}
}
}

View file

@ -49,6 +49,8 @@ export default function DhTemplateEnricher(match, _options) {
}
export const renderMeasuredTemplate = async event => {
const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes;
const button = event.currentTarget,
type = button.dataset.type,
range = button.dataset.range,
@ -57,13 +59,9 @@ export const renderMeasuredTemplate = async event => {
if (!type || !range || !game.canvas.scene) return;
const usedType = type === 'inFront' ? 'cone' : type === 'emanation' ? 'circle' : type;
const usedType = type === 'inFront' ? 'cone' : type;
const usedAngle =
type === CONST.MEASURED_TEMPLATE_TYPES.CONE
? (angle ?? CONFIG.MeasuredTemplate.defaults.angle)
: type === CONFIG.DH.GENERAL.templateTypes.INFRONT
? '180'
: undefined;
type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined;
let baseDistance = range;
if (Number.isNaN(Number(range))) {
@ -71,18 +69,49 @@ export const renderMeasuredTemplate = async event => {
range
];
}
const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance;
const dimensionConstant = game.scenes.active.grid.size / game.scenes.active.grid.distance;
baseDistance *= dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === LINE ? 5 * dimensionConstant : type === RECTANGLE ? length : undefined;
const { width, height } = game.canvas.scene.dimensions;
const data = {
const shapeData = {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: usedType,
distance: distance,
width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
direction: direction
radius: radius,
direction: direction,
type: usedType
};
CONFIG.ux.TemplateManager.createPreview(data);
await canvas.regions.placeRegion(
{
name: usedType.capitalize(),
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [],
displayMeasurements: true,
locked: false,
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
visibility: CONST.REGION_VISIBILITY.ALWAYS
},
{ create: true }
);
};

View file

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

View file

@ -60,7 +60,13 @@ export const getCommandTarget = (options = {}) => {
export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, hopeFaces, fearFaces, advantageFaces) => {
if (!game.modules.get('dice-so-nice')?.active) return;
const diceSoNicePresets = await getDiceSoNicePresets(hopeFaces, fearFaces, advantageFaces, advantageFaces);
const diceSoNicePresets = await getDiceSoNicePresets(
rollResult,
hopeFaces,
fearFaces,
advantageFaces,
advantageFaces
);
rollResult.dice[0].options = diceSoNicePresets.hope;
rollResult.dice[1].options = diceSoNicePresets.fear;
if (rollResult.dice[2] && advantageState) {
@ -171,10 +177,10 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue
[innerProperty]: innerPropertyDefaultValue
};
} else {
acc[`${key}.-=${innerProperty}`] = null;
acc[`${key}.${innerProperty}`] = _del;
}
} else {
acc[`-=${key}`] = null;
acc[`${key}`] = _del;
}
return acc;
@ -416,7 +422,12 @@ export async function createEmbeddedItemWithEffects(actor, baseData, update) {
...baseData,
id: data.id,
uuid: data.uuid,
effects: data.effects?.map(effect => effect.toObject())
_uuid: data.uuid,
effects: data.effects?.map(effect => effect.toObject()),
_stats: {
...data._stats,
compendiumSource: data.pack ? `Compendium.${data.pack}.Item.${data.id}` : null
}
}
]);
@ -469,6 +480,8 @@ export async function waitForDiceSoNice(message) {
}
export function refreshIsAllowed(allowedTypes, typeToCheck) {
if (!allowedTypes) return true;
switch (typeToCheck) {
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
case CONFIG.DH.GENERAL.refreshTypes.session.id:
@ -485,6 +498,34 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
}
}
function expireActiveEffectIsAllowed(allowedTypes, typeToCheck) {
if (typeToCheck === CONFIG.DH.GENERAL.activeEffectDurations.act.id) return true;
return refreshIsAllowed(allowedTypes, typeToCheck);
}
export function expireActiveEffects(actor, allowedTypes = null) {
const shouldExpireEffects = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).autoExpireActiveEffects;
if (!shouldExpireEffects) return;
const effectsToExpire = actor
.getActiveEffects()
.filter(effect => {
if (!effect.system?.duration.type) return false;
const { temporary, custom } = CONFIG.DH.GENERAL.activeEffectDurations;
if ([temporary.id, custom.id].includes(effect.system.duration.type)) return false;
return expireActiveEffectIsAllowed(allowedTypes, effect.system.duration.type);
})
.map(x => x.id);
actor.deleteEmbeddedDocuments('ActiveEffect', effectsToExpire);
}
export async function getCritDamageBonus(formula) {
const critRoll = new Roll(formula);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
@ -497,6 +538,16 @@ export function htmlToText(html) {
return tempDivElement.textContent || tempDivElement.innerText || '';
}
export function getIconVisibleActiveEffects(effects) {
return effects.filter(effect => {
if (!(effect instanceof game.system.api.documents.DhActiveEffect)) return true;
const alwaysShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.ALWAYS;
const conditionalShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.CONDITIONAL && !effect.transfer; // TODO: system specific logic
return !effect.disabled && (alwaysShown || conditionalShown);
});
}
export async function getFeaturesHTMLData(features) {
const result = [];
for (const feature of features) {
@ -582,6 +633,8 @@ export async function RefreshFeatures(
const refreshedActors = {};
for (let actor of game.actors) {
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
expireActiveEffects(actor, refreshTypes);
const updates = {};
for (let item of actor.items) {
if (
@ -676,3 +729,16 @@ export async function RefreshFeatures(
return refreshedActors;
}
export function getUnusedDamageTypes(parts) {
const usedKeys = Object.keys(parts);
return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => {
if (!usedKeys.includes(key))
acc.push({
value: key,
label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label)
});
return acc;
}, []);
}

View file

@ -46,6 +46,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/button-part.hbs',
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
'systems/daggerheart/templates/scene/dh-config.hbs'
'systems/daggerheart/templates/scene/dh-config.hbs',
'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs'
]);
};

View file

@ -197,7 +197,7 @@ export async function runMigrations() {
const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator);
const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => {
if (!game.actors.some(actor => actor.id === id)) {
acc[`-=${id}`] = null;
acc[id] = _del;
}
return acc;
}, {});

View file

@ -1,6 +1,13 @@
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhCountdowns from '../data/countdowns.mjs';
import { DhAppearance, DhAutomation, DhHomebrew, DhMetagaming, DhVariantRules } from '../data/settings/_module.mjs';
import {
DhAppearance,
DhAutomation,
DhGlobalOverrides,
DhHomebrew,
DhMetagaming,
DhVariantRules
} from '../data/settings/_module.mjs';
import {
DhAppearanceSettings,
DhAutomationSettings,
@ -54,17 +61,18 @@ const registerMenuSettings = () => {
}
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.GlobalOverrides, {
scope: 'world',
config: false,
type: DhGlobalOverrides
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, {
scope: 'client',
config: false,
type: DhAppearance,
onChange: value => {
if (value.displayFear) {
if (ui.resources) {
if (value.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}
value.handleChange();
}
});
};

View file

@ -91,8 +91,8 @@
"useDefault": false
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -118,7 +118,7 @@
},
"base": false
}
],
},
"includeBase": false
},
"_id": "TCKVaVweyJzhEArX",
@ -343,7 +343,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -400,18 +400,18 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "act"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"tint": "#ffffff",
@ -423,6 +423,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!89yAh30vaNQOALlz.ctXYwil2D1zfsekT.9PsnogEPsp1OOK64"
}
],
@ -461,8 +471,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -489,7 +499,7 @@
}
}
},
{
"armor": {
"value": {
"custom": {
"enabled": true,
@ -514,7 +524,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -588,8 +598,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -616,7 +626,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -642,8 +652,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -670,7 +680,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -75,8 +75,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -102,7 +102,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -400,8 +400,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -427,7 +427,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -508,7 +508,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -581,8 +581,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -608,7 +608,7 @@
}
}
},
{
"hope": {
"value": {
"custom": {
"enabled": true,
@ -633,7 +633,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -72,8 +72,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -100,7 +100,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-bone-black.webp",
"type": "attack",

View file

@ -85,8 +85,8 @@
},
"range": "far",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -112,7 +112,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/unholy/beam-ringed-impact-purple.webp",
"type": "attack",
@ -256,7 +256,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -336,8 +336,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -363,7 +363,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -414,8 +414,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -440,7 +440,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -488,18 +488,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p>Until you roll with Hope.</p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p><em>Vulnerable</em> until you roll with Hope.</p>",
"tint": "#ffffff",
@ -511,6 +512,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!WPEOIGfclNJxWb87.4EECsXzHFG0RoIg0.KGdf2eqcXkdigg0u"
}
],
@ -608,7 +619,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -681,8 +692,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -708,7 +719,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/bows/longbow-recurve-leather-brown.webp",
"type": "attack",
@ -246,8 +246,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -273,7 +273,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -68,8 +68,8 @@
"description": "<p>A group of trained archers bearing massive bows.</p>",
"attack": {
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -95,7 +95,7 @@
"resultBased": false,
"base": false
}
]
}
},
"name": "Longbow",
"img": "icons/weapons/bows/longbow-recurve-leather-brown.webp",
@ -270,8 +270,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -295,7 +295,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -368,8 +368,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -393,7 +393,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -81,8 +81,8 @@
},
"range": "close",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -108,7 +108,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false
@ -277,20 +277,21 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p>Until you clear a HP.</p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p> <em>Vulnerable</em> until you clear a HP.</p>",
"description": "<p><em>Vulnerable</em> until you clear a HP.</p>",
"tint": "#ffffff",
"statuses": [
"vulnerable"
@ -300,6 +301,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!h5RuhzGL17dW5FBT.Fz2lnUEeBxsDpx0G.2iBVUGHtGW3I9VIj"
}
],

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"range": "melee",
"type": "attack",
@ -309,7 +309,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -382,8 +382,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -409,7 +409,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -482,8 +482,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -509,7 +509,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -582,8 +582,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -609,7 +609,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -667,20 +667,21 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until their next roll with Hope.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p><em>Vulnerable</em> until your next roll with Hope. </p>",
"description": "<p><em>Vulnerable</em> until your next roll with Hope.</p>",
"tint": "#ffffff",
"statuses": [
"vulnerable"
@ -690,6 +691,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!dgH3fW9FTYLaIDvS.XtnByqUr9AuYU9Ip.9NQcCXMhjyBReJRd"
}
],
@ -726,8 +737,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -752,7 +763,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -825,7 +836,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -883,20 +894,21 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until the cube is defeated.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p><em>Vulnerable</em> until the cube is defeated. </p>",
"description": "<p><em>Vulnerable</em> until the cube is defeated.</p>",
"tint": "#ffffff",
"statuses": [
"vulnerable"
@ -906,6 +918,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!dgH3fW9FTYLaIDvS.ijIaKjroxq3xZd9Z.S7kJlhnV8Nexzi8l"
}
],
@ -942,8 +964,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -969,7 +991,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -1049,8 +1071,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -1075,7 +1097,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -1143,8 +1165,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -1170,7 +1192,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"img": "icons/creatures/claws/claw-straight-brown.webp",
"type": "attack",
@ -284,8 +284,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -311,7 +311,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -368,18 +368,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p>Until you break free with a successful Strength Roll.</p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>You are <em>Restrained</em> until you break free with a successful Strength Roll.</p>",
"tint": "#ffffff",
@ -391,6 +392,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!71qKDLKO3CsrNkdy.zgR0MEqyobKp2yXr.U50Ccm9emMqAxma6"
}
],
@ -430,8 +441,8 @@
"consumeOnSuccess": false
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -457,7 +468,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -80,8 +80,8 @@
"name": "Longsword",
"img": "icons/weapons/swords/sword-guard.webp",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",
@ -246,7 +246,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -319,7 +319,7 @@
"recovery": null
},
"damage": {
"parts": [],
"parts": {},
"includeBase": false
},
"target": {
@ -376,18 +376,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until you break free with a successful attack, Finesse Roll, or Strength Roll.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>You are <em>Restrained</em> until you break free with a successful attack, Finesse Roll, or Strength Roll.</p>",
"tint": "#ffffff",
@ -399,6 +400,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!B4LZcGuBAHzyVdzy.9gizFt9ovKL05DXu.LmzztuktRkwOCy1a"
}
],

View file

@ -83,8 +83,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -110,7 +110,7 @@
},
"base": false
}
]
}
},
"img": "icons/skills/melee/unarmed-punch-fist-yellow-red.webp",
"type": "attack",
@ -280,8 +280,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -307,7 +307,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -389,8 +389,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -415,7 +415,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -452,18 +452,18 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"tint": "#ffffff",
@ -475,6 +475,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!2UeZ0tEe7AzgSJNd.69reUZ5tv3splqyO.CjMrSdL6kgD8mKRQ"
}
],

View file

@ -79,8 +79,8 @@
},
"range": "veryClose",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
],
},
"direct": true
},
"name": "Club",
@ -336,8 +336,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false,
@ -365,7 +365,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},
@ -412,8 +412,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"fear": {
"value": {
"custom": {
"enabled": true,
@ -438,7 +438,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -507,8 +507,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -534,7 +534,7 @@
}
}
}
],
},
"includeBase": false,
"direct": true
},

View file

@ -74,8 +74,8 @@
},
"range": "close",
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/light/beam-rays-magenta.webp",
"type": "attack",
@ -383,8 +383,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -410,7 +410,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -483,8 +483,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": false
@ -508,7 +508,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -67,8 +67,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -95,7 +95,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"chatDisplay": false

View file

@ -72,8 +72,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -99,7 +99,7 @@
},
"base": false
}
]
}
},
"name": "Fist Slam",
"img": "icons/skills/melee/unarmed-punch-fist-yellow-red.webp",
@ -332,8 +332,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -359,7 +359,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -534,8 +534,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -561,7 +561,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-straight-cracked.webp",
"type": "attack",
@ -256,8 +256,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -282,7 +282,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -320,18 +320,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until the scene ends or they succeed on a social action against the </span><span style=\"box-sizing: border-box; scrollbar-width: thin; scrollbar-color: rgb(93, 20, 43) rgba(0, 0, 0, 0); font-family: Montserrat, sans-serif; color: rgb(239, 230, 216); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\">Courtesan</span><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p><em>Vulnerable</em> until the scene ends or they succeed on a social action against the Courtesan.</p>",
"tint": "#ffffff",
@ -343,6 +344,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!ZxWaWPdzFIUPNC62.rSMUPC5GhR982ifg.blcRqns0PHqiuPac"
}
],

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/weapons/daggers/dagger-twin-green.webp",
"type": "attack",
@ -253,8 +253,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -279,7 +279,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {
@ -336,18 +336,18 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "scene"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"tint": "#ffffff",
@ -359,6 +359,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!CBBuEXAlLKFMJdjg.LYNaKEYcYMgvF4Rf.YNMhgBZW8ndrCjIp"
}
],

View file

@ -84,8 +84,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -111,7 +111,7 @@
},
"base": false
}
]
}
},
"range": "far",
"img": "icons/weapons/staves/staff-ornate-purple.webp",
@ -256,8 +256,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -283,7 +283,7 @@
}
}
},
{
"stress": {
"value": {
"custom": {
"enabled": true,
@ -308,7 +308,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -422,31 +422,32 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [
{
"key": "system.resistance.magical.resistance",
"value": 1,
"priority": null,
"type": "override"
},
{
"key": "system.resistance.physical.resistance",
"value": 1,
"priority": null,
"type": "override"
}
],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until the </span><span style=\"box-sizing: border-box; scrollbar-width: thin; scrollbar-color: rgb(93, 20, 43) rgba(0, 0, 0, 0); font-family: Montserrat, sans-serif; color: rgb(239, 230, 216); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\">Cult Adept</span><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\"> marks their last HP.</span></p>"
}
},
"changes": [
{
"key": "system.resistance.magical.resistance",
"mode": 5,
"value": "1",
"priority": null
},
{
"key": "system.resistance.physical.resistance",
"mode": 5,
"value": "1",
"priority": null
}
],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>Resistance to all damage until the Adept marks their last HP</p>",
"tint": "#ffffff",
@ -456,6 +457,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!0NxCSugvKQ4W8OYZ.IHWDn097sRgjlZXO.U9lWz1LgeAiK5L85"
}
],
@ -533,18 +544,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until you break free with a successful Strength or Instinct Roll.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>You are<em> Restrained</em> in smoky chains until you break free with a successful Strength or Instinct Roll. A target Restrained by this feature must spend a Hope to make an action roll.</p>",
"tint": "#ffffff",
@ -556,6 +568,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!0NxCSugvKQ4W8OYZ.JpSrduK3vjd9h098.lNH6srSPyEprXZ4o"
}
],
@ -592,8 +614,8 @@
"recovery": "scene"
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -618,7 +640,7 @@
},
"type": []
}
],
},
"includeBase": false
},
"target": {

View file

@ -74,8 +74,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -101,7 +101,7 @@
},
"base": false
}
]
}
},
"range": "melee",
"type": "attack",
@ -300,8 +300,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -326,7 +326,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -384,18 +384,18 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"tint": "#ffffff",
@ -407,6 +407,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!tyBOpLfigAhI9bU3.ohASSruBxcvuItIK.LwWxRz7FTMA80VdA"
}
],

View file

@ -66,8 +66,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": true,
@ -94,7 +94,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",

View file

@ -79,8 +79,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -106,7 +106,7 @@
},
"base": false
}
]
}
},
"img": "icons/magic/nature/root-vines-grow-brown.webp",
"type": "attack",
@ -245,8 +245,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"stress": {
"value": {
"custom": {
"enabled": true,
@ -271,7 +271,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -325,8 +325,8 @@
"recovery": null
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -350,7 +350,7 @@
}
}
}
],
},
"includeBase": false
},
"target": {
@ -407,18 +407,19 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until the </span><span style=\"box-sizing: border-box; scrollbar-width: thin; scrollbar-color: rgb(93, 20, 43) rgba(0, 0, 0, 0); font-family: Montserrat, sans-serif; color: rgb(239, 230, 216); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\">Deeproot Defender</span><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\"> takes Severe damage.</span></p>"
}
},
"changes": [],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>You are <em>Restrained </em>until the Defender takes Severe damage.</p>",
"tint": "#ffffff",
@ -430,6 +431,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!9x2xY9zwc3xzbXo5.rreGFW5TbhUoZf2T.F3E7fiz01AbF2kr5"
}
],

View file

@ -80,8 +80,8 @@
"type": "attack"
},
"damage": {
"parts": [
{
"parts": {
"hitPoints": {
"value": {
"custom": {
"enabled": false
@ -107,7 +107,7 @@
},
"base": false
}
]
}
},
"type": "attack",
"range": "melee",

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