[Fix] 1633 - ActiveEffect Autocomplete (#1636)

* Improved the autocomplete typing experience

* Made it work. But I hate it.

* Revert "Made it work. But I hate it."

This reverts commit d2fc9fd648.

* Actually nice solution instead O_O
This commit is contained in:
WBHarry 2026-02-06 11:32:33 +01:00 committed by GitHub
parent 0d2495c143
commit c1f7866594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 139 additions and 102 deletions

View file

@ -54,7 +54,11 @@ export default class AttributionDialog extends HandlebarsApplicationMixin(Applic
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -70,7 +70,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label);
return element;
@ -119,7 +123,11 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label);
return element;

View file

@ -4,20 +4,34 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
constructor(options) {
super(options);
const ignoredActorKeys = ['config', 'DhEnvironment'];
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
attributes.value.push(...maxAttributes);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
if (ignoredActorKeys.includes(key)) return acc;
const model = game.system.api.models.actors[key];
const group = game.i18n.localize(model.metadata.label);
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type);
const getLabel = path => {
const label = model.schema.getField(path)?.label;
return label ? game.i18n.localize(label) : path;
};
const bars = attributes.bar.flatMap(x => {
const joined = `${x.join('.')}.max`;
const label =
joined === 'resources.hope.max'
? 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'
: getLabel(joined);
return { value: joined, label, group };
});
const values = attributes.value.flatMap(x => {
const joined = x.join('.');
return { value: joined, label: getLabel(joined), group };
});
acc.push(...bars, ...values);
return acc;
}, []);
}
@ -68,14 +82,18 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const matchIndex = label.toLowerCase().indexOf(search.toLowerCase());
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}`;
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -103,7 +103,11 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML = `${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`;
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}

View file

@ -40,7 +40,14 @@ export default class DhpAdversary extends BaseDataActor {
integer: true,
label: 'DAGGERHEART.GENERAL.hordeHp'
}),
criticalThreshold: new fields.NumberField({ required: true, integer: true, min: 1, max: 20, initial: 20 }),
criticalThreshold: new fields.NumberField({
required: true,
integer: true,
min: 1,
max: 20,
initial: 20,
label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold'
}),
damageThresholds: new fields.SchemaField({
major: new fields.NumberField({
required: true,

View file

@ -35,15 +35,18 @@ export default class DhCharacter extends BaseDataActor {
'DAGGERHEART.ACTORS.Character.maxHPBonus'
),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField({
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
}),
isReversed: new fields.BooleanField({ initial: false })
})
hope: new fields.SchemaField(
{
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
}),
isReversed: new fields.BooleanField({ initial: false })
},
{ label: 'DAGGERHEART.GENERAL.hope' }
)
}),
traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),

View file

@ -7,16 +7,20 @@ const attributeField = label =>
});
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
new fields.SchemaField({
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
});
new fields.SchemaField(
{
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ??
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
},
{ label }
);
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({

View file

@ -6,7 +6,12 @@ export default class DhLevelData extends foundry.abstract.DataModel {
return {
level: new fields.SchemaField({
current: new fields.NumberField({ required: true, integer: true, initial: 1 }),
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.GENERAL.currentLevel'
}),
changed: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false }))
}),

View file

@ -1,78 +1,30 @@
export default class DHToken extends CONFIG.Token.documentClass {
/**
* Inspect the Actor data model and identify the set of attributes which could be used for a Token Bar.
* @param {object} attributes The tracked attributes which can be chosen from
* @returns {object} A nested object of attribute choices to display
*/
static getTrackedAttributeChoices(attributes, model) {
/**@inheritdoc */
static getTrackedAttributeChoices(attributes, typeKey) {
attributes = attributes || this.getTrackedAttributes();
const barGroup = game.i18n.localize('TOKEN.BarAttributes');
const valueGroup = game.i18n.localize('TOKEN.BarValues');
const actorModel = typeKey ? game.system.api.data.actors[`Dh${typeKey.capitalize()}`] : null;
const getLabel = path => {
const label = actorModel.schema.getField(path)?.label;
return label ? game.i18n.localize(label) : path;
};
const bars = attributes.bar.map(v => {
const a = v.join('.');
const modelLabel = model ? game.i18n.localize(model.schema.getField(`${a}.value`).label) : null;
return { group: barGroup, value: a, label: modelLabel ? modelLabel : a };
return { group: barGroup, value: a, label: getLabel(a) };
});
bars.sort((a, b) => a.label.compare(b.label));
bars.sort((a, b) => a.value.compare(b.value));
const invalidAttributes = [
'gold',
'levelData',
'actions',
'biography',
'class',
'multiclass',
'companion',
'notes',
'partner',
'description',
'impulses',
'tier',
'type'
];
const values = attributes.value.reduce((acc, v) => {
const values = attributes.value.map(v => {
const a = v.join('.');
if (invalidAttributes.some(x => a.startsWith(x))) return acc;
const field = model ? model.schema.getField(a) : null;
const modelLabel = field ? game.i18n.localize(field.label) : null;
const hint = field ? game.i18n.localize(field.hint) : null;
acc.push({ group: valueGroup, value: a, label: modelLabel ? modelLabel : a, hint: hint });
return acc;
}, []);
values.sort((a, b) => a.label.compare(b.label));
return { group: valueGroup, value: a, label: getLabel(a) };
});
values.sort((a, b) => a.value.compare(b.value));
return bars.concat(values);
}
static _getTrackedAttributesFromSchema(schema, _path = []) {
const attributes = { bar: [], value: [] };
for (const [name, field] of Object.entries(schema.fields)) {
const p = _path.concat([name]);
if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p);
if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice)
attributes.value.push(p);
if (field instanceof foundry.data.fields.StringField) attributes.value.push(p);
if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p);
const isSchema = field instanceof foundry.data.fields.SchemaField;
const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
if (isSchema || isModel) {
const schema = isModel ? field.model.schema : field;
const isBar = schema.has && schema.has('value') && schema.has('max');
if (isBar) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(schema, p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
}
return attributes;
}
_shouldRecordMovementHistory() {
return false;
}