Merge branch 'v14-Dev' into feature/tag-team-partial-rendering

This commit is contained in:
WBHarry 2026-03-22 02:02:56 +01:00
commit df7d8bcd23
102 changed files with 1990 additions and 771 deletions

View file

@ -1,4 +1,4 @@
import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject;
this.actor = actor;
this.damage = damage;
this.damageType = damageType;
this.rulesDefault = game.settings.get(
CONFIG.DH.id,
@ -20,14 +21,20 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.rulesDefault
);
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
const maxArmorMarks = canApplyArmor ? availableArmor : 0;
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled);
const armor = orderedArmorSources.reduce((acc, { document }) => {
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData;
acc.push({
effect: document,
marks: [...Array(max).keys()].reduce((acc, _, index) => {
const spent = index < current;
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
return acc;
}, {})
});
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc;
}, {});
}, []);
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
(acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
@ -121,13 +128,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
this.getDamageInfo();
context.armorScore = this.actor.system.armorScore;
context.armorScore = this.actor.system.armorScore.max;
context.armorMarks = currentMarks;
context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0)
@ -141,16 +146,30 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
: null;
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
context.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
context.maxArmorUsed = maxArmorUsed;
context.availableArmor = availableArmor;
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
return acc;
}, {}),
const armorSources = [];
for (const source of this.marks.armor) {
const parent = source.effect.origin
? await foundry.utils.fromUuid(source.effect.origin)
: source.effect.parent;
const useEffectName = parent.type === 'armor' || parent instanceof Actor;
const label = useEffectName ? source.effect.name : parent.name;
armorSources.push({
label: label,
uuid: source.effect.uuid,
marks: source.marks
});
}
context.marks = {
armor: armorSources,
stress: this.marks.stress
};
context.usesStressArmor = Object.keys(context.marks.stress).length;
context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage);
@ -167,27 +186,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected));
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected)
: [];
const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length;
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
const availableArmor =
maxArmorUsed -
this.marks.armor.reduce((acc, source) => {
acc += Object.values(source.marks).filter(x => x.selected).length;
return acc;
}, 0);
const armorMarkReduction =
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
let currentDamage = Math.max(
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
}
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
};
static toggleRules() {
@ -195,13 +218,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
this.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
armor: this.marks.armor.map((mark, index) => {
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
return acc;
}, {}),
return { ...mark, selected: keepSelectValue ? mark.selected : false };
}),
stress: this.marks.stress
};
@ -209,8 +229,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
static setMarks(_, target) {
const currentMark = this.marks[target.dataset.type][target.dataset.key];
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
if (!currentMark.selected && currentDamage === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
@ -218,12 +238,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
if (this.rulesOn) {
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
return;
}
}
const stressUsed = selectedStressMarks.length;
if (target.dataset.type === 'armor' && stressUsed) {
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
if (updateResult === false) return;
}
if (currentMark.selected) {
const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) {
@ -232,8 +258,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
}
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
selectedStressMarks.forEach(mark => (mark.selected = false));
if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
for (const source of this.marks.armor) {
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
if (match) {
source.marks[match].selected = false;
break;
}
}
currentMark.armorMarkId = null;
}
}
@ -241,6 +275,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.render();
}
updateStressArmor(armorMarkId, select) {
let stressMarkKey = null;
if (select) {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
);
} else {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].armorMarkId === armorMarkId
);
if (!stressMarkKey)
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
}
if (!stressMarkKey) return false;
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
}
static useStressReduction(_, target) {
const damageValue = Number(target.dataset.reduction);
const stressReduction = this.availableStressReductions[damageValue];
@ -279,11 +332,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}
static async takeDamage() {
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorChanges = this.marks.armor.reduce((acc, source) => {
const amount = Object.values(source.marks).filter(x => x.selected).length;
if (amount) acc.push({ uuid: source.effect.uuid, amount });
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
return acc;
}, []);
const stressSpent =
selectedStressMarks.filter(x => x.armorMarkId).length +
stressReductions.reduce((acc, red) => acc + red.cost, 0);
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
await this.close(true);
}

View file

@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const msg = {
user: game.user.id,
system: {
moves: moves,
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })),
actor: this.actor.uuid
},
speaker: cls.getSpeaker(),

View file

@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs';
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';

View file

@ -24,9 +24,12 @@ export default class DHActionConfig extends DHActionBaseConfig {
const effectData = this._addEffectData.bind(this)();
const data = this.action.toObject();
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
const created = await game.system.api.documents.DhActiveEffect.createDialog(effectData, {
parent: this.action.item,
render: false
});
if (!created) return;
data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true);

View file

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

View file

@ -150,6 +150,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
minLength: 0
});
});
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
htmlElement
.querySelector('.armor-damage-thresholds-checkbox')
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
}
async _prepareContext(options) {
@ -186,21 +194,66 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all(
foundry.utils
.deepClone(context.source.changes)
.map((c, i) => this._prepareChangeContext(c, i, fields))
);
const singleTypes = ['armor'];
const typedChanges = context.source.changes.reduce((acc, change, index) => {
if (singleTypes.includes(change.type)) {
acc[change.type] = { ...change, index };
}
return acc;
}, {});
partContext.changes = partContext.changes.filter(c => !!c);
partContext.typedChanges = typedChanges;
break;
}
return partContext;
}
_prepareChangeContext(change, index, fields) {
armorChangeToggle(event) {
if (event.target.checked) {
this.addArmorChange();
} else {
this.removeTypedChange(event.target.dataset.index);
}
}
/* Could be generalised if needed later */
addArmorChange() {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue());
return this.submit({ updateData: { system: { changes } } });
}
removeTypedChange(indexString) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system.changes);
const index = Number(indexString);
changes.splice(index, 1);
return this.submit({ updateData: { system: { changes } } });
}
armorDamageThresholdToggle(event) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
const index = Number(event.target.dataset.index);
if (event.target.checked) {
changes[index].value.damageThresholds = { major: 0, severe: 0 };
} else {
changes[index].value.damageThresholds = null;
}
return this.submit({ updateData: { system: { changes } } });
}
/** @inheritdoc */
_renderChange(context) {
const { change, index, defaultPriority } = context;
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
@ -220,7 +273,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
change,
index,
defaultPriority,
fields
fields,
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
return r;
}, {})
}
)
);
@ -247,4 +304,34 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return submitData;
}
/** @inheritDoc */
_processSubmitData(event, form, submitData, options) {
if (this.options.isSetting) {
// Settings should update source instead
this.document.updateSource(submitData);
this.render();
} else {
return super._processSubmitData(event, form, submitData, options);
}
}
/** Creates an active effect config for a setting */
static async configureSetting(effect, options = {}) {
const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id });
return new Promise(resolve => {
const app = new this({ document, ...options, isSetting: true });
app.addEventListener(
'close',
() => {
const newEffect = app.document.toObject(true);
newEffect.id = newEffect._id;
delete newEffect._id;
resolve(newEffect);
},
{ once: true }
);
app.render({ force: true });
});
}
}

View file

@ -1,223 +0,0 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(effect) {
super({});
this.effect = foundry.utils.deepClone(effect);
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
tag: 'form',
position: {
width: 560
},
form: {
submitOnChange: false,
closeOnSubmit: false,
handler: SettingActiveEffectConfig.#onSubmit
},
actions: {
editImage: SettingActiveEffectConfig.#editImage,
addChange: SettingActiveEffectConfig.#addChange,
deleteChange: SettingActiveEffectConfig.#deleteChange
}
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
/**@inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.source = this.effect;
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
return context;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) {
if (partId in context.tabs) context.tab = context.tabs[partId];
switch (partId) {
case 'details':
context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) }));
context.isActorEffect = false;
context.isItemEffect = true;
const useGeneric = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
context.statuses = [
...context.statuses,
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}))
];
}
break;
case 'changes':
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
return modes;
}, {});
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
break;
}
return context;
}
static async #onSubmit(_event, _form, formData) {
this.data = foundry.utils.expandObject(formData.object);
this.close();
}
/**
* Edit a Document image.
* @this {DocumentSheetV2}
* @type {ApplicationClickAction}
*/
static async #editImage(_event, target) {
if (target.nodeName !== 'IMG') {
throw new Error('The editImage action is available only for IMG elements.');
}
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.effect, attr);
const fp = new FilePicker.implementation({
current,
type: 'image',
callback: path => (target.src = path),
position: {
top: this.position.top + 40,
left: this.position.left + 10
}
});
await fp.browse();
}
/**
* Add a new change to the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #addChange() {
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(changes ?? {});
updatedChanges.push({});
this.effect = { ...rest, changes: updatedChanges };
this.render();
}
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #deleteChange(event) {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(submitData.changes);
const row = event.target.closest('li');
const index = Number(row.dataset.index) || 0;
updatedChanges.splice(index, 1);
this.effect = { ...submitData, changes: updatedChanges };
this.render();
}
static async configure(effect, options = {}) {
return new Promise(resolve => {
const app = new this(effect, options);
app.addEventListener('close', () => resolve(app.data), { once: true });
app.render({ force: true });
});
}
}

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -34,7 +34,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty
viewParty: CharacterSheet.#viewParty,
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
},
window: {
resizable: true,
@ -638,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
const inputValue = Number(event.currentTarget.value);
const { value, max } = this.document.system.armorScore;
const changeValue = Math.min(inputValue - value, max - value);
const maxMarks = this.document.system.armorScore;
const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue;
this.document.system.updateArmorValue({ value: changeValue });
}
/* -------------------------------------------- */
@ -803,10 +804,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Toggles ArmorScore resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmor(_, button, element) {
const ArmorValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
await this.document.system.armor.update({ 'system.marks.value': newValue });
static async #toggleArmor(_, button, _element) {
const { value, max } = this.document.system.armorScore;
const inputValue = Number.parseInt(button.dataset.value);
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
const changeValue = Math.min(newValue - value, max - value);
this.document.system.updateArmorValue({ value: changeValue });
}
/**
@ -932,6 +936,99 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
static async #toggleArmorManagement(_event, target) {
const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container');
if (existingTooltip) {
game.tooltip.dismissLockedTooltips();
return;
}
const armorSources = getArmorSources(this.document)
.filter(s => !s.disabled)
.toReversed()
.map(({ name, document, data }) => ({
...data,
uuid: document.uuid,
name
}));
if (!armorSources.length) return;
const useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
const html = document.createElement('div');
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`,
{
sources: armorSources,
useResourcePips
}
);
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
direction: 'DOWN'
});
html.querySelectorAll('.armor-slot').forEach(element => {
element.addEventListener('click', CharacterSheet.armorSourcePipUpdate);
});
}
static async armorSourceInput(event) {
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
event.target.value = value;
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
progressBar.value = value;
}
/** Update specific armor source */
static async armorSourcePipUpdate(event) {
const target = event.target.closest('.armor-slot');
const { uuid, value } = target.dataset;
const document = await foundry.utils.fromUuid(uuid);
let inputValue = Number.parseInt(value);
let decreasing = false;
let newCurrent = 0;
if (document.type === 'armor') {
decreasing = document.system.armor.current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
await document.update({ 'system.armor.current': newCurrent });
} else if (document.system.armorData) {
const { current } = document.system.armorData;
decreasing = current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
const newChanges = document.system.changes.map(change => ({
...change,
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
}));
await document.update({ 'system.changes': newChanges });
} else {
return;
}
const container = target.closest('.slot-bar');
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
const index = Number.parseInt(armorSlot.dataset.index);
if (decreasing && index >= newCurrent) {
armorSlot.classList.remove('fa-shield');
armorSlot.classList.add('fa-shield-halved');
} else if (!decreasing && index < newCurrent) {
armorSlot.classList.add('fa-shield');
armorSlot.classList.remove('fa-shield-halved');
}
}
}
static async #toggleResourceManagement(event, button) {
event.stopPropagation();
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
@ -965,7 +1062,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
);
const target = button.closest('.resource-section');
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,

View file

@ -189,11 +189,14 @@ export default class Party extends DHBaseActorSheet {
* Toggles a armor slot resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmorSlot(_, target, element) {
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const armorValue = Number.parseInt(target.dataset.value);
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
await armorItem.update({ 'system.marks.value': newValue });
static async #toggleArmorSlot(_, target) {
const actor = game.actors.get(target.dataset.actorId);
const { value, max } = actor.system.armorScore;
const inputValue = Number.parseInt(target.dataset.value);
const newValue = value >= inputValue ? inputValue - 1 : inputValue;
const changeValue = Math.min(newValue - value, max - value);
await actor.system.updateArmorValue({ value: changeValue });
this.render();
}

View file

@ -749,11 +749,13 @@ export default function DHApplicationMixin(Base) {
const cls =
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
const data = {
name: cls.defaultName({ type, parent }),
type,
system: systemData
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {

View file

@ -160,7 +160,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
inactives: []
};
for (const effect of this.actor.allApplicableEffects()) {
for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) {
const list = effect.active ? context.effects.actives : context.effects.inactives;
list.push(effect);
}

View file

@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
return context;
}
async updateArmorEffect(event) {
const value = Number.parseInt(event.target.value);
const armorEffect = this.document.system.armorEffect;
if (Number.isNaN(value) || !armorEffect) return;
await armorEffect.system.armorChange.updateArmorMax(value);
this.render();
}
/**
* Callback function used by `tagifyElement`.
* @param {Array<Object>} selectedOptions - The currently selected tag objects.

View file

@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs';
export { default as DhProgress } from './progress.mjs';

View file

@ -0,0 +1,27 @@
export default class DhProgress {
#notification;
constructor({ max, label = '' }) {
this.max = max;
this.label = label;
this.#notification = ui.notifications.info(this.label, { progress: true });
}
updateMax(newMax) {
this.max = newMax;
}
advance({ by = 1, label = this.label } = {}) {
if (this.value === this.max) return;
this.value = (this.value ?? 0) + Math.abs(by);
this.#notification.update({ message: label, pct: this.value / this.max });
}
close({ label = '' } = {}) {
this.#notification.update({ message: label, pct: 1 });
}
static createMigrationProgress(max = 0) {
return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') });
}
}