175 lines
4.6 KiB
JavaScript
175 lines
4.6 KiB
JavaScript
import { isValidID, toID } from "../../utils/toID.mjs";
|
|
import { __ID__ } from "../../consts.mjs";
|
|
import { clamp } from "../../utils/clamp.mjs";
|
|
|
|
const { getProperty, hasProperty, setProperty } = foundry.utils;
|
|
|
|
export class AttributeItemData extends foundry.abstract.TypeDataModel {
|
|
// MARK: Schema
|
|
static defineSchema() {
|
|
const fields = foundry.data.fields;
|
|
return {
|
|
group: new fields.StringField({
|
|
blank: false,
|
|
trim: true,
|
|
nullable: true,
|
|
initial: null,
|
|
}),
|
|
key: new fields.StringField({
|
|
blank: false,
|
|
trim: true,
|
|
nullable: false,
|
|
}),
|
|
aboveTheFold: new fields.BooleanField({
|
|
initial: false,
|
|
}),
|
|
trigger: new fields.DocumentUUIDField({
|
|
embedded: false,
|
|
relative: false,
|
|
type: foundry.documents.Macro.documentName,
|
|
}),
|
|
|
|
/* The attributes current value */
|
|
value: new fields.NumberField({
|
|
integer: true,
|
|
}),
|
|
/* The minimum accepted value */
|
|
min: new fields.NumberField({
|
|
integer: true,
|
|
}),
|
|
/* The maximum accepted value */
|
|
max: new fields.NumberField({
|
|
integer: true,
|
|
}),
|
|
};
|
|
};
|
|
|
|
// #region Lifecycle
|
|
async _preCreate(data, options, user) {
|
|
// Prevent users from creating attributes if disallowed
|
|
if (
|
|
!game.settings.get(__ID__, `canPlayersManageAttributes`)
|
|
&& !user.isGM
|
|
) {
|
|
ui.notifications.error(_loc(`taf.notifs.error.cant-manage-attributes`));
|
|
return false;
|
|
};
|
|
|
|
// Assign the key as the ID'd name if isn't provided, or validate if
|
|
// it is provided.
|
|
if (!this.key) {
|
|
this.updateSource({ key: toID(this.parent.name) });
|
|
} else if (!isValidID(this.key)) {
|
|
ui.notifications.error(_loc(
|
|
`taf.notifs.error.invalid-attribute-key`,
|
|
{ key: this.key },
|
|
));
|
|
return false;
|
|
};
|
|
|
|
// Prevent duplicate Attribute keys from existing on a single Actor
|
|
if (this.parent.isEmbedded) {
|
|
const attr = this.parent.parent?.getAttribute(this.key);
|
|
if (attr) {
|
|
ui.notifications.error(
|
|
`taf.notifs.error.duplicate-attribute-key`,
|
|
{
|
|
localize: true,
|
|
format: { key: this.key },
|
|
},
|
|
);
|
|
return false;
|
|
};
|
|
};
|
|
|
|
return super._preCreate(data, options, user);
|
|
};
|
|
|
|
async _preUpdate(data, options, user) {
|
|
const allowed = await super._preUpdate(data, options, user);
|
|
if (allowed === false) { return false };
|
|
|
|
// Prevent invalid IDs
|
|
if (hasProperty(data, `system.key`) && !isValidID(data.system.key)) {
|
|
ui.notifications.error(_loc(
|
|
`taf.notifs.error.invalid-attribute-key`,
|
|
{ key: data.system.key },
|
|
));
|
|
delete data.system?.key;
|
|
};
|
|
|
|
// Prevent value going out of the bounds of min/max
|
|
if (hasProperty(data, `system.value`)) {
|
|
const value = getProperty(data, `system.value`);
|
|
const max = getProperty(data, `system.max`) ?? this.max;
|
|
|
|
let min = getProperty(data, `system.min`) ?? this.min;
|
|
if (max != null) { min ??= 0 };
|
|
|
|
setProperty(data, `system.value`, clamp(min, value, max));
|
|
};
|
|
};
|
|
// #endregion Lifecycle
|
|
|
|
// #region Methods
|
|
get isRange() {
|
|
return this.max !== null;
|
|
};
|
|
|
|
get inferredMinimum() {
|
|
if (this.isRange) {
|
|
return this.min ?? 0;
|
|
};
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Executes the macro associated with this item, if the macro cannot be
|
|
* found or if the user does not permission to execute it, it will not be
|
|
* executed. This also provides some extra context into the roll data for chat
|
|
* macros, so that they can refer to the min/value/max properties of this
|
|
* specific item without actually needing to know which item called the macro.
|
|
*/
|
|
async execute() {
|
|
const macro = await fromUuid(this.trigger);
|
|
if (!macro || !macro.canExecute) { return };
|
|
|
|
// Provide the chat-specific context when required
|
|
if (macro.type === `chat`) {
|
|
const extraContext = {
|
|
name: this.parent.name,
|
|
min: this.min,
|
|
value: this.value,
|
|
max: this.max,
|
|
};
|
|
|
|
Hooks.once(`taf.getRollData`, (data) => {
|
|
data.active = extraContext;
|
|
});
|
|
|
|
// Apply any roll data additions to the message flavour as well
|
|
// since that doesn't get formatted by the ChatLog
|
|
Hooks.once(`preCreateChatMessage`, (message) => {
|
|
if (message.flavor.includes(`@active`)) {
|
|
const flavor = message.flavor.replaceAll(
|
|
/@active\.(\w+)/g,
|
|
(fullMatch, key) => {
|
|
return extraContext[key] || fullMatch;
|
|
},
|
|
);
|
|
message.updateSource({ flavor });
|
|
};
|
|
});
|
|
};
|
|
|
|
// Get the speaker so that Foundry has the correct context to be able to call
|
|
// the Actor's getData method, letting us augment the context dynamically for
|
|
// the @active roll context
|
|
const speaker = foundry.documents.ChatMessage.implementation.getSpeaker({
|
|
actor: this.parent.parent,
|
|
});
|
|
|
|
await macro?.execute({ item: this.parent, speaker });
|
|
};
|
|
// #endregion Methods
|
|
};
|