mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-04-22 07:23:37 +02:00
Merged with v14-Dev
This commit is contained in:
commit
6bf0fffcb7
735 changed files with 9587 additions and 6016 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { itemAbleRollParse } from '../helpers/utils.mjs';
|
||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
||||
import { RefreshType } from '../systemRegistration/socket.mjs';
|
||||
|
||||
export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
||||
/* -------------------------------------------- */
|
||||
|
|
@ -8,6 +8,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
|
||||
/**@override */
|
||||
get isSuppressed() {
|
||||
if (this.system.isSuppressed === true) return true;
|
||||
|
||||
// If this is a copied effect from an attachment, never suppress it
|
||||
// (These effects have attachmentSource metadata)
|
||||
if (this.flags?.daggerheart?.attachmentSource) {
|
||||
|
|
@ -15,7 +17,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
}
|
||||
|
||||
// Then apply the standard suppression rules
|
||||
if (['weapon', 'armor'].includes(this.parent?.type)) {
|
||||
if (['weapon', 'armor'].includes(this.parent?.type) && this.transfer) {
|
||||
return !this.parent.system.equipped;
|
||||
}
|
||||
|
||||
|
|
@ -50,10 +52,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 = types || ['base'];
|
||||
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
|
||||
const labelKey = `TYPES.ActiveEffect.${type}`;
|
||||
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;
|
||||
|
||||
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 = {};
|
||||
|
|
@ -61,6 +108,18 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
update.img = 'icons/magic/life/heart-cross-blue.webp';
|
||||
}
|
||||
|
||||
const existingEffect = this.actor.effects.find(x => x.origin === data.origin);
|
||||
const stacks = Boolean(data.system?.stacking);
|
||||
if (existingEffect && !stacks) return false;
|
||||
|
||||
if (existingEffect && stacks) {
|
||||
const incrementedValue = existingEffect.system.stacking.value + 1;
|
||||
await existingEffect.update({
|
||||
'system.stacking.value': Math.min(incrementedValue, existingEffect.system.stacking.max ?? Infinity)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const statuses = Object.keys(data.statuses ?? {});
|
||||
const immuneStatuses =
|
||||
statuses.filter(
|
||||
|
|
@ -109,23 +168,24 @@ 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) {
|
||||
static _applyChangeUnguided(actor, change, changes, options) {
|
||||
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
|
||||
super._applyLegacy(actor, change, changes);
|
||||
super._applyChangeUnguided(actor, change, changes, options);
|
||||
}
|
||||
|
||||
/** */
|
||||
static getChangeValue(model, change, effect) {
|
||||
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 +196,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
|
|||
} catch (_) {}
|
||||
}
|
||||
|
||||
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
|
||||
return evalValue ?? value;
|
||||
const stackingParsedValue = effect.system.stacking
|
||||
? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value })
|
||||
: key;
|
||||
const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent);
|
||||
return evalValue ?? key;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs';
|
|||
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
|
||||
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
|
||||
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
|
||||
import { abilities } from '../config/actorConfig.mjs';
|
||||
|
||||
export default class DhpActor extends Actor {
|
||||
parties = new Set();
|
||||
|
|
@ -29,6 +30,18 @@ export default class DhpActor extends Actor {
|
|||
return this.system.metadata.isNPC;
|
||||
}
|
||||
|
||||
prepareData() {
|
||||
super.prepareData();
|
||||
|
||||
// Update effects if it is the user's character or is controlled
|
||||
if (canvas.ready) {
|
||||
const controlled = canvas.tokens.controlled.some(t => t.actor === this);
|
||||
if (game.user.character === this || controlled) {
|
||||
ui.effectsDisplay.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
|
|
@ -142,7 +155,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;
|
||||
}, {});
|
||||
|
|
@ -187,7 +200,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;
|
||||
}, {})
|
||||
});
|
||||
|
|
@ -509,6 +522,30 @@ export default class DhpActor extends Actor {
|
|||
return await rollClass.build(config);
|
||||
}
|
||||
|
||||
async rollTrait(trait, options = {}) {
|
||||
const abilityLabel = game.i18n.localize(abilities[trait].label);
|
||||
const config = {
|
||||
event: event,
|
||||
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this),
|
||||
roll: {
|
||||
trait: trait,
|
||||
type: 'trait'
|
||||
},
|
||||
hasRoll: true,
|
||||
actionType: 'action',
|
||||
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
|
||||
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
|
||||
ability: abilityLabel
|
||||
}),
|
||||
...options
|
||||
};
|
||||
return await this.diceRoll(config);
|
||||
}
|
||||
|
||||
get rollClass() {
|
||||
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
|
||||
}
|
||||
|
|
@ -573,8 +610,7 @@ export default class DhpActor extends Actor {
|
|||
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
|
||||
|
||||
const canUseArmor =
|
||||
this.system.armor &&
|
||||
this.system.armor.system.marks.value < this.system.armorScore &&
|
||||
this.system.armorScore.value < this.system.armorScore.max &&
|
||||
type.every(t => this.system.armorApplicableDamageTypes[t] === true);
|
||||
const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
|
||||
const rule = stressDamageReduction[x];
|
||||
|
|
@ -614,12 +650,7 @@ export default class DhpActor extends Actor {
|
|||
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
|
||||
if (hpDamage?.value) {
|
||||
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
|
||||
if (
|
||||
this.type === 'character' &&
|
||||
!isDirect &&
|
||||
this.system.armor &&
|
||||
this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)
|
||||
) {
|
||||
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
|
||||
const armorSlotResult = await this.owner.query(
|
||||
'armorSlot',
|
||||
{
|
||||
|
|
@ -632,12 +663,10 @@ export default class DhpActor extends Actor {
|
|||
}
|
||||
);
|
||||
if (armorSlotResult) {
|
||||
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult;
|
||||
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
|
||||
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
|
||||
if (armorSpent) {
|
||||
const armorUpdate = updates.find(u => u.key === 'armor');
|
||||
if (armorUpdate) armorUpdate.value += armorSpent;
|
||||
else updates.push({ value: armorSpent, key: 'armor' });
|
||||
for (const armorChange of armorChanges) {
|
||||
updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
|
||||
}
|
||||
if (stressSpent) {
|
||||
const stressUpdate = updates.find(u => u.key === 'stress');
|
||||
|
|
@ -774,12 +803,8 @@ export default class DhpActor extends Actor {
|
|||
);
|
||||
break;
|
||||
case 'armor':
|
||||
if (this.system.armor?.system?.marks) {
|
||||
updates.armor.resources['system.marks.value'] = Math.max(
|
||||
Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore),
|
||||
0
|
||||
);
|
||||
}
|
||||
if (!r.uuid) this.system.updateArmorValue(r);
|
||||
else this.system.updateArmorEffectValue(r);
|
||||
break;
|
||||
default:
|
||||
if (this.system.resources?.[r.key]) {
|
||||
|
|
@ -995,4 +1020,20 @@ export default class DhpActor extends Actor {
|
|||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
/**@inheritdoc */
|
||||
*allApplicableEffects({ noSelfArmor, noTransferArmor } = {}) {
|
||||
for (const effect of this.effects) {
|
||||
if (!noSelfArmor || effect.type !== 'armor') yield effect;
|
||||
}
|
||||
for (const item of this.items) {
|
||||
for (const effect of item.effects) {
|
||||
if (effect.transfer && (!noTransferArmor || effect.type !== 'armor')) yield effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyActiveEffects(phase) {
|
||||
super.applyActiveEffects(phase);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
|
|||
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
|
||||
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
|
||||
}
|
||||
|
||||
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
|
||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||
action: socketEvent.Refresh,
|
||||
data: {
|
||||
refreshType: RefreshType.TagTeamRoll
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onApplyDamage(event) {
|
||||
|
|
|
|||
|
|
@ -230,4 +230,14 @@ export default class DHItem extends foundry.documents.Item {
|
|||
async _preDelete() {
|
||||
this.deleteTriggers();
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
const documentClass = game.system.api.data.items[`DH${source.type?.capitalize()}`];
|
||||
if (documentClass?.migrateDocumentData) {
|
||||
documentClass.migrateDocumentData(source);
|
||||
}
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export default class DhRollTable extends foundry.documents.RollTable {
|
|||
}
|
||||
|
||||
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
|
||||
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
|
||||
messageOptions.rollMode ??= game.settings.get('core', 'messageMode');
|
||||
|
||||
// Construct chat data
|
||||
messageData = foundry.utils.mergeObject(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue