Merge pull request 'Attribute Item Subtype' (#76) from feature/attribute-items into main
Reviewed-on: #76
This commit is contained in:
commit
0347a00632
31 changed files with 1048 additions and 180 deletions
37
.vscode/handlebars.code-snippets
vendored
37
.vscode/handlebars.code-snippets
vendored
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
// Place your foundry.dungeon workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
|
||||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
|
||||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
|
||||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
|
||||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
|
||||||
// Placeholders with the same ids are connected.
|
|
||||||
// Example:
|
|
||||||
// "Print to console": {
|
|
||||||
// "scope": "javascript,typescript",
|
|
||||||
// "prefix": "log",
|
|
||||||
// "body": [
|
|
||||||
// "console.log('$1');",
|
|
||||||
// "$2"
|
|
||||||
// ],
|
|
||||||
// "description": "Log output to console"
|
|
||||||
// }
|
|
||||||
"Localization Shortcut (concat)": {
|
|
||||||
"scope": "handlebars,html",
|
|
||||||
"prefix": "i18n",
|
|
||||||
"body": ["localize (concat \"dotdungeon.$1\" $2)"]
|
|
||||||
},
|
|
||||||
"Localization Shortcut (no concat)": {
|
|
||||||
"scope": "handlebars,html",
|
|
||||||
"prefix": "i18n",
|
|
||||||
"body": ["localize \"dotdungeon.$1\""]
|
|
||||||
},
|
|
||||||
"Icon": {
|
|
||||||
"scope": "handlebars,html",
|
|
||||||
"prefix": "icon",
|
|
||||||
"body": [
|
|
||||||
"<div aria-hidden=\"true\" class=\"icon icon--${1:20}\">",
|
|
||||||
"\t{{{ $2 }}}",
|
|
||||||
"</div>"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
"player": "Player"
|
"player": "Player"
|
||||||
},
|
},
|
||||||
"Item": {
|
"Item": {
|
||||||
"generic": "Generic Item"
|
"generic": "Item",
|
||||||
|
"attribute": "Attribute"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taf": {
|
"taf": {
|
||||||
|
|
@ -48,7 +49,8 @@
|
||||||
"PlayerSheet": "Player Sheet",
|
"PlayerSheet": "Player Sheet",
|
||||||
"SingleModePlayerSheet": "Player Sheet (Always Editing)",
|
"SingleModePlayerSheet": "Player Sheet (Always Editing)",
|
||||||
"AttributeOnlyPlayerSheet": "Player Sheet (Attributes Only)",
|
"AttributeOnlyPlayerSheet": "Player Sheet (Attributes Only)",
|
||||||
"GenericItemSheet": "System Item Sheet"
|
"GenericItemSheet": "System Item Sheet",
|
||||||
|
"AttributeItemSheet": "Attribute Sheet"
|
||||||
},
|
},
|
||||||
"misc": {
|
"misc": {
|
||||||
"Key": "Key",
|
"Key": "Key",
|
||||||
|
|
@ -67,6 +69,15 @@
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"equipped": "Equipped",
|
"equipped": "Equipped",
|
||||||
"group": "Group"
|
"group": "Group"
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"key": {
|
||||||
|
"hint": "This is the computer-friendly identifier for the attribute. When accessing the attribute in rolls, this is the name you will need to use. Changing this WILL break any existing macros you have."
|
||||||
|
},
|
||||||
|
"always-visible": "Always Visible",
|
||||||
|
"minimum": "Minimum",
|
||||||
|
"current-value": "Current Value",
|
||||||
|
"maximum": "Maximum"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Apps": {
|
"Apps": {
|
||||||
|
|
@ -97,6 +108,7 @@
|
||||||
"toggle-item-description": "Show/Hide Item Description",
|
"toggle-item-description": "Show/Hide Item Description",
|
||||||
"tab-names": {
|
"tab-names": {
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
|
"attributes": "Attributes",
|
||||||
"items": "Items"
|
"items": "Items"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -133,10 +145,15 @@
|
||||||
"missing-id": "An ID must be provided",
|
"missing-id": "An ID must be provided",
|
||||||
"invalid-socket": "Invalid socket data received, this means a module or system bug is present.",
|
"invalid-socket": "Invalid socket data received, this means a module or system bug is present.",
|
||||||
"unknown-socket-event": "An unknown socket event was received: {event}",
|
"unknown-socket-event": "An unknown socket event was received: {event}",
|
||||||
"malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}"
|
"duplicate-attribute-key": "Cannot create an Attribute with a key (\"{key}\") that already exists on the Actor",
|
||||||
|
"invalid-attribute-key": "Attribute keys must be alphanumeric (a-z, 0-9) with underscores, invalid key provided: \"{key}\""
|
||||||
|
},
|
||||||
|
"warn": {
|
||||||
|
"migration-in-progress": "Applying data migrations for version {version}. Please do NOT refresh the window while this warning is present."
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"saved-default-attributes": "Successfully saved default Actor attributes"
|
"saved-default-attributes": "Successfully saved default Actor attributes",
|
||||||
|
"migration-successful": "Data migrations for version {version} were successful."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import { PlayerSheet } from "./apps/PlayerSheet.mjs";
|
||||||
import { QueryStatus } from "./apps/QueryStatus.mjs";
|
import { QueryStatus } from "./apps/QueryStatus.mjs";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
import { isValidID, toID } from "./utils/toID.mjs";
|
||||||
import { attributeSorter } from "./utils/attributeSort.mjs";
|
import { attributeSorter } from "./utils/attributeSort.mjs";
|
||||||
import { DialogManager } from "./utils/DialogManager.mjs";
|
import { DialogManager } from "./utils/DialogManager.mjs";
|
||||||
import { localizer } from "./utils/localizer.mjs";
|
import { localizer } from "./utils/localizer.mjs";
|
||||||
import { QueryManager } from "./utils/QueryManager.mjs";
|
import { QueryManager } from "./utils/QueryManager.mjs";
|
||||||
import { toID } from "./utils/toID.mjs";
|
|
||||||
|
|
||||||
const { deepFreeze } = foundry.utils;
|
const { deepFreeze } = foundry.utils;
|
||||||
|
|
||||||
|
|
@ -26,5 +26,6 @@ export const api = deepFreeze({
|
||||||
attributeSorter,
|
attributeSorter,
|
||||||
localizer,
|
localizer,
|
||||||
toID,
|
toID,
|
||||||
|
isValidID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
74
module/apps/AttributeItemSheet.mjs
Normal file
74
module/apps/AttributeItemSheet.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { TAFDocumentSheetMixin } from "./mixins/TAFDocumentSheetMixin.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
const { ItemSheetV2 } = foundry.applications.sheets;
|
||||||
|
|
||||||
|
export class AttributeItemSheet extends
|
||||||
|
TAFDocumentSheetMixin(
|
||||||
|
HandlebarsApplicationMixin(
|
||||||
|
ItemSheetV2,
|
||||||
|
)) {
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`AttributeItemSheet`,
|
||||||
|
],
|
||||||
|
position: {
|
||||||
|
width: 350,
|
||||||
|
height: `auto`,
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
submitOnChange: true,
|
||||||
|
closeOnSubmit: false,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
header: { template: filePath(`templates/AttributeItemSheet/header.hbs`) },
|
||||||
|
value: { template: filePath(`templates/AttributeItemSheet/value.hbs`) },
|
||||||
|
settings: { template: filePath(`templates/AttributeItemSheet/settings.hbs`) },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tells the Application's TAFDocumentSheetMixin how to rerender
|
||||||
|
* this app when specific properties get changed on the actor, so that
|
||||||
|
* it doesn't need to full-app rendering if we can do a partial
|
||||||
|
* rerender instead.
|
||||||
|
*/
|
||||||
|
static PROPERTY_TO_PARTIAL = {
|
||||||
|
"name": [`header`],
|
||||||
|
"system.value": [`value`],
|
||||||
|
"system.min": [`value`],
|
||||||
|
"system.max": [`value`],
|
||||||
|
"system.aboveTheFold": [`settings`],
|
||||||
|
"system.group": [`settings`],
|
||||||
|
"system.key": [`settings`],
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Instance Data
|
||||||
|
// #endregion Instance Data
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _prepareContext() {
|
||||||
|
return {
|
||||||
|
meta: {
|
||||||
|
idp: this.id,
|
||||||
|
editable: this.isEditable,
|
||||||
|
limited: this.isLimited,
|
||||||
|
},
|
||||||
|
item: this.item,
|
||||||
|
system: this.item.system,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
import { deleteItemFromElement, editItemFromElement } from "./utils.mjs";
|
import { deleteItemFromElement, editItemFromElement } from "./utils.mjs";
|
||||||
import { AttributeManager } from "./AttributeManager.mjs";
|
import { AttributeManager } from "./AttributeManager.mjs";
|
||||||
import { attributeSorter } from "../utils/attributeSort.mjs";
|
|
||||||
import { config } from "../config.mjs";
|
import { config } from "../config.mjs";
|
||||||
import { Logger } from "../utils/Logger.mjs";
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
|
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
|
||||||
|
|
@ -45,14 +44,21 @@ export class PlayerSheet extends
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
header: { template: filePath(`templates/PlayerSheet/header.hbs`) },
|
header: { template: filePath(`templates/PlayerSheet/header.hbs`) },
|
||||||
attributes: { template: filePath(`templates/PlayerSheet/attributes.hbs`) },
|
primaryAttributes: { template: filePath(`templates/PlayerSheet/primary-attributes.hbs`) },
|
||||||
tabs: { template: filePath(`templates/generic/tabs.hbs`) },
|
tabs: { template: filePath(`templates/generic/tabs.hbs`) },
|
||||||
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
|
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
|
||||||
items: {
|
attributeTab: {
|
||||||
template: filePath(`templates/PlayerSheet/item-lists.hbs`),
|
template: filePath(`templates/PlayerSheet/tabs/attributes/lists.hbs`),
|
||||||
scrollable: [``],
|
scrollable: [``],
|
||||||
templates: [
|
templates: [
|
||||||
filePath(`templates/PlayerSheet/item.hbs`),
|
filePath(`templates/PlayerSheet/tabs/attributes/attribute.hbs`),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
template: filePath(`templates/PlayerSheet/tabs/items/lists.hbs`),
|
||||||
|
scrollable: [``],
|
||||||
|
templates: [
|
||||||
|
filePath(`templates/PlayerSheet/tabs/items/item.hbs`),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -78,6 +84,7 @@ export class PlayerSheet extends
|
||||||
labelPrefix: `taf.Apps.PlayerSheet.tab-names`,
|
labelPrefix: `taf.Apps.PlayerSheet.tab-names`,
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: `content` },
|
{ id: `content` },
|
||||||
|
{ id: `attributes` },
|
||||||
{ id: `items` },
|
{ id: `items` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -104,6 +111,10 @@ export class PlayerSheet extends
|
||||||
Logger.debug(`Asserting app "${this.id}" from tab "items" to "${initial}"`);
|
Logger.debug(`Asserting app "${this.id}" from tab "items" to "${initial}"`);
|
||||||
this.tabGroups.primary = initial;
|
this.tabGroups.primary = initial;
|
||||||
};
|
};
|
||||||
|
if (this.tabGroups.primary === `attributes` && !this.hasAttributesTab) {
|
||||||
|
Logger.debug(`Asserting app "${this.id}" from tab "attributes" to "${initial}"`);
|
||||||
|
this.tabGroups.primary = initial;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -118,6 +129,7 @@ export class PlayerSheet extends
|
||||||
switch (tabID) {
|
switch (tabID) {
|
||||||
case `content`: return this.hasContentTab;
|
case `content`: return this.hasContentTab;
|
||||||
case `items`: return this.hasItemsTab;
|
case `items`: return this.hasItemsTab;
|
||||||
|
case `attributes`: return this.hasAttributesTab;
|
||||||
};
|
};
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -126,8 +138,17 @@ export class PlayerSheet extends
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
get hasAttributesTab() {
|
||||||
|
return this.actor.itemTypes
|
||||||
|
.attribute
|
||||||
|
?.filter(attr => !attr.system.aboveTheFold)
|
||||||
|
.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
get hasItemsTab() {
|
get hasItemsTab() {
|
||||||
return this.actor.items.size > 0;
|
return this.actor.items
|
||||||
|
.filter(item => item.type !== `attribute`)
|
||||||
|
.length > 0;
|
||||||
};
|
};
|
||||||
// #endregion Instance Data
|
// #endregion Instance Data
|
||||||
|
|
||||||
|
|
@ -207,7 +228,7 @@ export class PlayerSheet extends
|
||||||
|
|
||||||
new ContextMenu.implementation(
|
new ContextMenu.implementation(
|
||||||
this.element,
|
this.element,
|
||||||
`li.item`,
|
`[data-item-uuid]`,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: _loc(`taf.misc.edit`),
|
label: _loc(`taf.misc.edit`),
|
||||||
|
|
@ -254,8 +275,12 @@ export class PlayerSheet extends
|
||||||
|
|
||||||
async _preparePartContext(partID, ctx) {
|
async _preparePartContext(partID, ctx) {
|
||||||
switch (partID) {
|
switch (partID) {
|
||||||
case `attributes`: {
|
case `primaryAttributes`: {
|
||||||
await this._prepareAttributes(ctx);
|
await this._preparePrimaryAttributes(ctx);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
case `attributeTab`: {
|
||||||
|
await this._prepareAttributesTab(ctx);
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
case `tabs`: {
|
case `tabs`: {
|
||||||
|
|
@ -275,18 +300,35 @@ export class PlayerSheet extends
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
async _prepareAttributes(ctx) {
|
async _preparePrimaryAttributes(ctx) {
|
||||||
ctx.hasAttributes = this.actor.system.hasAttributes;
|
const attrs = this.actor.itemTypes.attribute ?? [];
|
||||||
|
const filtered = attrs.filter(attr => attr.system.aboveTheFold);
|
||||||
|
ctx.hasAttributes = filtered.length > 0;
|
||||||
|
ctx.attrs = filtered;
|
||||||
|
};
|
||||||
|
|
||||||
const attrs = [];
|
async _prepareAttributesTab(ctx) {
|
||||||
for (const [id, data] of Object.entries(this.actor.system.attr)) {
|
ctx.tabActive = this.tabGroups.primary === `attributes`;
|
||||||
attrs.push({
|
|
||||||
...data,
|
const groups = new Map();
|
||||||
id,
|
const attrs = (this.actor.itemTypes.attribute ?? [])
|
||||||
path: `system.attr.${id}`,
|
.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
|
for (const attr of attrs) {
|
||||||
|
if (attr.system.aboveTheFold) { continue };
|
||||||
|
|
||||||
|
const groupName = attr.system.group ?? `Attributes`;
|
||||||
|
if (!groups.has(groupName)) {
|
||||||
|
groups.set(groupName, {
|
||||||
|
name: groupName.titleCase(),
|
||||||
|
attrs: [],
|
||||||
|
collapsed: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
ctx.attrs = attrs.toSorted(attributeSorter);
|
const group = groups.get(groupName);
|
||||||
|
|
||||||
|
group.attrs.push(attr);
|
||||||
|
};
|
||||||
|
ctx.attrGroups = [...groups.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
async _prepareTabList(ctx) {
|
async _prepareTabList(ctx) {
|
||||||
|
|
@ -321,19 +363,24 @@ export class PlayerSheet extends
|
||||||
|
|
||||||
ctx.itemGroups = [];
|
ctx.itemGroups = [];
|
||||||
for (const [groupName, items] of Object.entries(this.actor.itemTypes)) {
|
for (const [groupName, items] of Object.entries(this.actor.itemTypes)) {
|
||||||
|
|
||||||
|
// We don't care about attribute items here
|
||||||
|
if (groupName === `attribute`) { continue };
|
||||||
|
|
||||||
const preparedItems = [];
|
const preparedItems = [];
|
||||||
|
|
||||||
let summedWeight = 0;
|
let summedWeight = 0;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
summedWeight += item.system.quantifiedWeight;
|
summedWeight += item.system.quantifiedWeight ?? 0;
|
||||||
preparedItems.push(await this._prepareItem(item));
|
const data = await this._prepareItem(item);
|
||||||
|
if (data) { preparedItems.push(data) };
|
||||||
};
|
};
|
||||||
totalWeight += summedWeight;
|
totalWeight += summedWeight;
|
||||||
|
|
||||||
ctx.itemGroups.push({
|
ctx.itemGroups.push({
|
||||||
name: groupName.titleCase(),
|
name: groupName.titleCase(),
|
||||||
items: preparedItems,
|
items: preparedItems,
|
||||||
weight: config.weightFormatter(totalWeight),
|
weight: config.weightFormatter(summedWeight),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -343,6 +390,7 @@ export class PlayerSheet extends
|
||||||
};
|
};
|
||||||
|
|
||||||
async _prepareItem(item) {
|
async _prepareItem(item) {
|
||||||
|
if (item.type !== `generic`) { return };
|
||||||
const ctx = {
|
const ctx = {
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
img: item.img,
|
img: item.img,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import { __ID__ } from "../../consts.mjs";
|
||||||
|
|
||||||
export class PlayerData extends foundry.abstract.TypeDataModel {
|
export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||||
|
// #region Schema
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
|
|
@ -12,24 +15,53 @@ export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
initial: null,
|
initial: null,
|
||||||
}),
|
}),
|
||||||
attr: new fields.TypedObjectField(
|
attr: new fields.ObjectField({ persisted: false, initial: {} }),
|
||||||
new fields.SchemaField({
|
|
||||||
name: new fields.StringField({ blank: false, trim: true }),
|
|
||||||
sort: new fields.NumberField({ min: 1, initial: 1, integer: true, nullable: false }),
|
|
||||||
value: new fields.NumberField({ min: 0, initial: 0, integer: true, nullable: false }),
|
|
||||||
max: new fields.NumberField({ min: 0, initial: null, integer: true, nullable: true }),
|
|
||||||
isRange: new fields.BooleanField({ initial: false, nullable: false }),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
initial: {},
|
|
||||||
nullable: false,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// #endregion Schema
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
/**
|
||||||
|
* This makes sure that the actor gets created with the global
|
||||||
|
* attributes if they exist, while still allowing programmatic
|
||||||
|
* creation through the API with specific attributes.
|
||||||
|
*/
|
||||||
|
async _preCreate(data, options, user) {
|
||||||
|
|
||||||
|
// Assign the default items from the world setting if required
|
||||||
|
const items = this.parent._source.items;
|
||||||
|
if (items.length === 0 && !options.cloning) {
|
||||||
|
const defaults = game.settings.get(__ID__, `actorDefaultAttributes`) ?? [];
|
||||||
|
this.parent.updateSource({ items: defaults });
|
||||||
|
};
|
||||||
|
|
||||||
|
return super._preCreate(data, options, user);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For every attribute item that the character has, we want that data
|
||||||
|
* accessible in the system data, so we create objects dynamically that
|
||||||
|
* the rest of Foundry can read in to emulate the attributes being on
|
||||||
|
* the Actor directly.
|
||||||
|
*/
|
||||||
|
prepareDerivedData() {
|
||||||
|
const attrs = this.parent.items?.filter(item => item.type === `attribute`);
|
||||||
|
for (const attr of attrs) {
|
||||||
|
if (attr.system.isRange) {
|
||||||
|
this.attr[attr.system.key] = {
|
||||||
|
value: attr.system.value,
|
||||||
|
max: attr.system.max,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.attr[attr.system.key] = attr.system.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Methods
|
||||||
get hasAttributes() {
|
get hasAttributes() {
|
||||||
return Object.keys(this.attr).length > 0;
|
return Object.keys(this.attr).length > 0;
|
||||||
};
|
};
|
||||||
|
// #endregion Methods
|
||||||
};
|
};
|
||||||
|
|
|
||||||
112
module/data/Item/attribute.mjs
Normal file
112
module/data/Item/attribute.mjs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { isValidID, toID } from "../../utils/toID.mjs";
|
||||||
|
import { clamp } from "../../utils/clamp.mjs";
|
||||||
|
|
||||||
|
const { getProperty, hasProperty, setProperty } = foundry.utils;
|
||||||
|
|
||||||
|
export class AttributeItemData extends foundry.abstract.TypeDataModel {
|
||||||
|
// #region 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,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/* 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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Schema
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _preCreate(data, options, user) {
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
// #endregion Methods
|
||||||
|
};
|
||||||
|
|
@ -1,29 +1,12 @@
|
||||||
import { __ID__ } from "../consts.mjs";
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
import { clamp } from "../utils/clamp.mjs";
|
||||||
|
|
||||||
const { Actor } = foundry.documents;
|
const { Actor } = foundry.documents;
|
||||||
const { hasProperty } = foundry.utils;
|
const { deepClone, setProperty } = foundry.utils;
|
||||||
|
|
||||||
export class TAFActor extends Actor {
|
export class TAFActor extends Actor {
|
||||||
|
|
||||||
// #region Lifecycle
|
// #region Lifecycle
|
||||||
/**
|
|
||||||
* This makes sure that the actor gets created with the global attributes if
|
|
||||||
* they exist, while still allowing programmatic creation through the API with
|
|
||||||
* specific attributes.
|
|
||||||
*/
|
|
||||||
async _preCreate(data, options, user) {
|
|
||||||
|
|
||||||
// Assign the defaults from the world setting if they exist
|
|
||||||
const defaults = game.settings.get(__ID__, `actorDefaultAttributes`) ?? {};
|
|
||||||
if (!hasProperty(data, `system.attr`)) {
|
|
||||||
// Remove with issue: Foundry/taf#55
|
|
||||||
const value = game.release.generation > 13 ? _replace(defaults) : defaults;
|
|
||||||
this.updateSource({ "system.==attr": value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return super._preCreate(data, options, user);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This resets the cache of the item groupings whenever a descedant document
|
* This resets the cache of the item groupings whenever a descedant document
|
||||||
* gets changed (created, updated, deleted) so that we keep the cache as close
|
* gets changed (created, updated, deleted) so that we keep the cache as close
|
||||||
|
|
@ -33,10 +16,43 @@ export class TAFActor extends Actor {
|
||||||
super._onEmbeddedDocumentChange(...args);
|
super._onEmbeddedDocumentChange(...args);
|
||||||
this.#sortedTypes = null;
|
this.#sortedTypes = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This override allows the _preCreate operations to see whether the actor is
|
||||||
|
* being cloned or created from nothing. This allows for easy one-time operations
|
||||||
|
* that should be performed during Actor creation but not duplication to occur.
|
||||||
|
*/
|
||||||
|
clone(data, context) {
|
||||||
|
context.cloning = true;
|
||||||
|
return super.clone(data, context);
|
||||||
|
};
|
||||||
// #endregion Lifecycle
|
// #endregion Lifecycle
|
||||||
|
|
||||||
// #region Token Attributes
|
// #region Token Attributes
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* This override exists in order to support making updates to the Actor's
|
||||||
|
* embedded attribute Items from the token, or falling back to the default
|
||||||
|
* handling if it's not one of our attributes.
|
||||||
|
*/
|
||||||
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
|
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
|
||||||
|
if (attribute.startsWith(`attr.`)) {
|
||||||
|
const key = attribute.slice(5);
|
||||||
|
const attr = this.getAttribute(key);
|
||||||
|
value = isDelta ? attr.system.value + value : value;
|
||||||
|
value = clamp(attr.system.min, value, attr.system.max);
|
||||||
|
const updates = { system: { value } };
|
||||||
|
|
||||||
|
const allowed = Hooks.call(
|
||||||
|
`modifyTokenAttribute`,
|
||||||
|
{ attribute, value, isDelta, isBar, isEmbedded: true },
|
||||||
|
updates,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
|
return allowed !== false ? await attr.update(updates) : this;
|
||||||
|
};
|
||||||
|
|
||||||
const attr = foundry.utils.getProperty(this.system, attribute);
|
const attr = foundry.utils.getProperty(this.system, attribute);
|
||||||
const current = isBar ? attr.value : attr;
|
const current = isBar ? attr.value : attr;
|
||||||
const update = isDelta ? current + value : value;
|
const update = isDelta ? current + value : value;
|
||||||
|
|
@ -44,18 +60,7 @@ export class TAFActor extends Actor {
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine the updates to make to the actor data
|
return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
|
||||||
let updates;
|
|
||||||
if (isBar) {
|
|
||||||
updates = {[`system.${attribute}.value`]: Math.clamp(update, 0, attr.max)};
|
|
||||||
} else {
|
|
||||||
updates = {[`system.${attribute}`]: update};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Allow a hook to override these changes
|
|
||||||
const allowed = Hooks.call(`modifyTokenAttribute`, {attribute, value, isDelta, isBar}, updates, this);
|
|
||||||
|
|
||||||
return allowed !== false ? this.update(updates) : this;
|
|
||||||
};
|
};
|
||||||
// #endregion Token Attributes
|
// #endregion Token Attributes
|
||||||
|
|
||||||
|
|
@ -88,8 +93,15 @@ export class TAFActor extends Actor {
|
||||||
};
|
};
|
||||||
// #endregion Roll Data
|
// #endregion Roll Data
|
||||||
|
|
||||||
// #region Getters
|
// #region Methods
|
||||||
#sortedTypes = null;
|
#sortedTypes = null;
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* This override is intended to allow the "generic" item subtype to instead
|
||||||
|
* populate the Item types based on their "Group" property, for any other item
|
||||||
|
* subtype this function operates the same way that the default Foundry
|
||||||
|
* implementation does.
|
||||||
|
*/
|
||||||
get itemTypes() {
|
get itemTypes() {
|
||||||
if (this.#sortedTypes) { return this.#sortedTypes };
|
if (this.#sortedTypes) { return this.#sortedTypes };
|
||||||
const types = {};
|
const types = {};
|
||||||
|
|
@ -105,5 +117,58 @@ export class TAFActor extends Actor {
|
||||||
};
|
};
|
||||||
return this.#sortedTypes = types;
|
return this.#sortedTypes = types;
|
||||||
};
|
};
|
||||||
// #endregion Getters
|
|
||||||
|
/**
|
||||||
|
* Retrieves an attribute Item from the actor, used to more easily
|
||||||
|
*
|
||||||
|
* @param {string} key The unique identifier of the attribute
|
||||||
|
* @returns The attribute's Item document, or undefined if not found
|
||||||
|
*/
|
||||||
|
getAttribute(key) {
|
||||||
|
const attrs = this.itemTypes.attribute ?? [];
|
||||||
|
return attrs.find(attr => attr.system.key === key);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an embedded attribute Item with a new value.
|
||||||
|
*
|
||||||
|
* @param {string} key The unique identifier of the attribute
|
||||||
|
* @param {number} value The value to set the attribute to
|
||||||
|
*/
|
||||||
|
async setAttributeValue(key, value) {
|
||||||
|
const item = this.getAttribute(key);
|
||||||
|
await item?.update({system: { value }});
|
||||||
|
};
|
||||||
|
// #endregion Methods
|
||||||
|
|
||||||
|
// #region Data Migration
|
||||||
|
/**
|
||||||
|
* This checks and performs all data migrations that the system requires, some
|
||||||
|
* of these are one-time only migrations, others of them will happen every time
|
||||||
|
* an Actor is updated.
|
||||||
|
*/
|
||||||
|
static migrateData(data, options) {
|
||||||
|
this.#migrateToAttributeItems(data, options);
|
||||||
|
return super.migrateData(data, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method handles checking if the Actor has attributes within it's raw
|
||||||
|
* system data model, which was where attributes were stored originally, if
|
||||||
|
* it detects the need for a migration, it stores the existing attribute data
|
||||||
|
* into a flag so that the v3.0.0 migration script can handle creating the
|
||||||
|
* data and removing the property from the Actor.
|
||||||
|
*/
|
||||||
|
static #migrateToAttributeItems(data, options) {
|
||||||
|
if (options.partial) { return }
|
||||||
|
const attr = data.system?.attr ?? {};
|
||||||
|
if (Object.keys(attr).length > 0) {
|
||||||
|
setProperty(
|
||||||
|
data,
|
||||||
|
`flags.${__ID__}.convertAttributesIntoItems`,
|
||||||
|
deepClone(attr),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Data Migration
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,6 @@ export class TAFTokenDocument extends TokenDocument {
|
||||||
|
|
||||||
if (`value` in data && `max` in data) {
|
if (`value` in data && `max` in data) {
|
||||||
let editable = hasProperty(system, `${attribute}.value`);
|
let editable = hasProperty(system, `${attribute}.value`);
|
||||||
const isRange = getProperty(system, `${attribute}.isRange`);
|
|
||||||
if (isRange) {
|
|
||||||
return {
|
return {
|
||||||
type: `bar`,
|
type: `bar`,
|
||||||
attribute,
|
attribute,
|
||||||
|
|
@ -92,14 +90,6 @@ export class TAFTokenDocument extends TokenDocument {
|
||||||
max: parseInt(data.max || 0),
|
max: parseInt(data.max || 0),
|
||||||
editable,
|
editable,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: `value`,
|
|
||||||
attribute: `${attribute}.value`,
|
|
||||||
value: Number(data.value),
|
|
||||||
editable,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Otherwise null
|
// Otherwise null
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// Apps
|
// Apps
|
||||||
|
import { AttributeItemSheet } from "../apps/AttributeItemSheet.mjs";
|
||||||
import { AttributeOnlyPlayerSheet } from "../apps/AttributeOnlyPlayerSheet.mjs";
|
import { AttributeOnlyPlayerSheet } from "../apps/AttributeOnlyPlayerSheet.mjs";
|
||||||
import { GenericItemSheet } from "../apps/GenericItemSheet.mjs";
|
import { GenericItemSheet } from "../apps/GenericItemSheet.mjs";
|
||||||
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
||||||
import { SingleModePlayerSheet } from "../apps/SingleModePlayerSheet.mjs";
|
import { SingleModePlayerSheet } from "../apps/SingleModePlayerSheet.mjs";
|
||||||
|
|
||||||
// Data Models
|
// Data Models
|
||||||
|
import { AttributeItemData } from "../data/Item/attribute.mjs";
|
||||||
import { GenericItemData } from "../data/Item/generic.mjs";
|
import { GenericItemData } from "../data/Item/generic.mjs";
|
||||||
import { PlayerData } from "../data/Actor/player.mjs";
|
import { PlayerData } from "../data/Actor/player.mjs";
|
||||||
|
|
||||||
|
|
@ -35,6 +37,7 @@ Hooks.on(`init`, () => {
|
||||||
// #region Data Models
|
// #region Data Models
|
||||||
CONFIG.Actor.dataModels.player = PlayerData;
|
CONFIG.Actor.dataModels.player = PlayerData;
|
||||||
CONFIG.Item.dataModels.generic = GenericItemData;
|
CONFIG.Item.dataModels.generic = GenericItemData;
|
||||||
|
CONFIG.Item.dataModels.attribute = AttributeItemData;
|
||||||
// #endregion Data Models
|
// #endregion Data Models
|
||||||
|
|
||||||
// #region Sheets
|
// #region Sheets
|
||||||
|
|
@ -61,10 +64,20 @@ Hooks.on(`init`, () => {
|
||||||
__ID__,
|
__ID__,
|
||||||
GenericItemSheet,
|
GenericItemSheet,
|
||||||
{
|
{
|
||||||
|
types: [`generic`],
|
||||||
makeDefault: true,
|
makeDefault: true,
|
||||||
label: `taf.sheet-names.GenericItemSheet`,
|
label: `taf.sheet-names.GenericItemSheet`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
foundry.documents.collections.Items.registerSheet(
|
||||||
|
__ID__,
|
||||||
|
AttributeItemSheet,
|
||||||
|
{
|
||||||
|
types: [`attribute`],
|
||||||
|
makeDefault: true,
|
||||||
|
label: `taf.sheet-names.AttributeItemSheet`,
|
||||||
|
},
|
||||||
|
);
|
||||||
// #endregion Sheets
|
// #endregion Sheets
|
||||||
|
|
||||||
registerWorldSettings();
|
registerWorldSettings();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
import { checkMigrations } from "../migrations/checkMigrations.mjs";
|
||||||
|
|
||||||
Hooks.on(`ready`, () => {
|
Hooks.on(`ready`, () => {
|
||||||
// Remove with issue: Foundry/taf#52
|
// Remove with issue: Foundry/taf#52
|
||||||
if (game.release.generation < 14 && globalThis._loc == null) {
|
if (game.release.generation < 14 && globalThis._loc == null) {
|
||||||
globalThis._loc = game.i18n.format.bind(game.i18n);
|
globalThis._loc = game.i18n.format.bind(game.i18n);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checkMigrations();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
24
module/migrations/checkMigrations.mjs
Normal file
24
module/migrations/checkMigrations.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { migrateTo3_0_0 } from "./v3.0.0.mjs";
|
||||||
|
|
||||||
|
const { isNewerVersion } = foundry.utils;
|
||||||
|
|
||||||
|
export async function checkMigrations() {
|
||||||
|
if (!game.user.isActiveGM) {
|
||||||
|
Logger.debug(`User not active GM, skipping data migrations`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationVersion = game.settings.get(__ID__, `migrationVersion`);
|
||||||
|
let updateVersion = !migrationVersion;
|
||||||
|
|
||||||
|
if (isNewerVersion(`3.0.0`, migrationVersion)) {
|
||||||
|
await migrateTo3_0_0();
|
||||||
|
updateVersion = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updateVersion) {
|
||||||
|
game.settings.set(__ID__, `migrationVersion`, game.system.version);
|
||||||
|
};
|
||||||
|
};
|
||||||
105
module/migrations/utils.mjs
Normal file
105
module/migrations/utils.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate the documents within a collection based on what
|
||||||
|
*
|
||||||
|
* This function was originally reproduced from [Draw Steel's codebase](https://github.com/MetaMorphic-Digital/draw-steel/blob/82a0a050da7c0d6d28c0cd283cf3b6915f47ee2a/src/module/data/migrations.mjs#L206-L226),
|
||||||
|
* with modifications to it to work better without
|
||||||
|
*
|
||||||
|
* @param collection The Collection of documents to update.
|
||||||
|
* @param flag The flag name to reference for if the document should be migrated.
|
||||||
|
* @param convertor The function that takes the document and performs the.
|
||||||
|
* transformations to get the required update data.
|
||||||
|
* @param options Options to configure how the method behaves.
|
||||||
|
* @param options.pack The compendium pack to update.
|
||||||
|
* @param options.parent Parent of the collection for embedded collections.
|
||||||
|
* @param options.update Whether or not this method should perform the update, or pass back the array of DB operations.
|
||||||
|
* @returns An array of batch operations to perform.
|
||||||
|
*/
|
||||||
|
export async function migrateCollection(
|
||||||
|
collection,
|
||||||
|
flag,
|
||||||
|
convertor,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const toMigrate = collection
|
||||||
|
.filter(doc => doc.getFlag(__ID__, flag))
|
||||||
|
.map(doc => {
|
||||||
|
const update = convertor(doc, options) ?? {};
|
||||||
|
update[`_id`] = doc._id;
|
||||||
|
|
||||||
|
// v13/v14+ compatibility shim
|
||||||
|
if (game.release.generation > 13) {
|
||||||
|
update[`flags.${__ID__}.${flag}`] = _del;
|
||||||
|
} else {
|
||||||
|
update[`flags.${__ID__}.-=${flag}`] = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return update;
|
||||||
|
})
|
||||||
|
.filter(data => !!data);
|
||||||
|
|
||||||
|
if (!options.update) {
|
||||||
|
return [{
|
||||||
|
action: `update`,
|
||||||
|
broadcast: true,
|
||||||
|
documentName: collection.documentName,
|
||||||
|
updates: toMigrate,
|
||||||
|
noHook: true,
|
||||||
|
pack: options.pack,
|
||||||
|
parent: options.parent,
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modify in batches of 100
|
||||||
|
const batches = Math.ceil(toMigrate.length / 100);
|
||||||
|
for (let i = 0; i < batches; i++) {
|
||||||
|
const updateData = toMigrate.slice(i * 100, (i + 1) * 100);
|
||||||
|
await collection.documentClass.updateDocuments(
|
||||||
|
updateData,
|
||||||
|
{
|
||||||
|
pack: options.pack,
|
||||||
|
parent: options.parent,
|
||||||
|
diff: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a compendium pack should be migrated during `migrateWorld`.
|
||||||
|
*
|
||||||
|
* This function was reproduced from [Draw Steel's codebase](https://github.com/MetaMorphic-Digital/draw-steel/blob/82a0a050da7c0d6d28c0cd283cf3b6915f47ee2a/src/module/data/migrations.mjs#L287-L302)
|
||||||
|
*
|
||||||
|
* @param pack The CompendiumPack document
|
||||||
|
* @returns {boolean} Whether or not the pack should be migrated
|
||||||
|
*/
|
||||||
|
export function shouldMigrateCompendium(pack, types = [`Actor`, `Item`]) {
|
||||||
|
// We only care about actor and item migrations
|
||||||
|
if (!types.includes(pack.documentName)) {return false}
|
||||||
|
|
||||||
|
// World compendiums should all be migrated, system ones should never by migrated
|
||||||
|
if (pack.metadata.packageType === `world`) {return true}
|
||||||
|
if (pack.metadata.packageType === `system`) {return false}
|
||||||
|
|
||||||
|
// Module compendiums should only be migrated if they don't have a download or manifest URL
|
||||||
|
const module = game.modules.get(pack.metadata.packageName);
|
||||||
|
return !module.download && !module.manifest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function finishMigrationWarning(warning, version) {
|
||||||
|
warning.update({ pct: 1 });
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
warning.remove();
|
||||||
|
ui.notifications.success(
|
||||||
|
`taf.notifs.success.migration-successful`,
|
||||||
|
{
|
||||||
|
localize: true,
|
||||||
|
format: { version },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
3_000,
|
||||||
|
);
|
||||||
|
};
|
||||||
120
module/migrations/v3.0.0.mjs
Normal file
120
module/migrations/v3.0.0.mjs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import {
|
||||||
|
finishMigrationWarning,
|
||||||
|
migrateCollection,
|
||||||
|
shouldMigrateCompendium,
|
||||||
|
} from "./utils.mjs";
|
||||||
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const flag = `convertAttributesIntoItems`;
|
||||||
|
const worldOperations = [];
|
||||||
|
let compendiumOperations = [];
|
||||||
|
|
||||||
|
export async function migrateTo3_0_0() {
|
||||||
|
Logger.debug(`Starting v3.0.0 data migration`);
|
||||||
|
const packsToMigrate = game.packs.filter(
|
||||||
|
(pack) => shouldMigrateCompendium(pack, [`Actor`]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const warning = ui.notifications.warn(
|
||||||
|
`taf.notifs.warn.migration-in-progress`,
|
||||||
|
{
|
||||||
|
localize: true,
|
||||||
|
format: { version: `3.0.0` },
|
||||||
|
progress: true,
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrating world actors
|
||||||
|
worldOperations.push(
|
||||||
|
...await migrateCollection(
|
||||||
|
game.actors,
|
||||||
|
flag,
|
||||||
|
handleMigratingActor,
|
||||||
|
{ update: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
warning.update({ pct: 0.25 });
|
||||||
|
|
||||||
|
// Migrating all of the relevant compendiums
|
||||||
|
for (const pack of packsToMigrate) {
|
||||||
|
await pack.getDocuments();
|
||||||
|
|
||||||
|
const wasLocked = pack.config.locked;
|
||||||
|
if (wasLocked) {await pack.configure({ locked: false })}
|
||||||
|
|
||||||
|
compendiumOperations.push(
|
||||||
|
...await migrateCollection(
|
||||||
|
pack,
|
||||||
|
flag,
|
||||||
|
handleMigratingActor,
|
||||||
|
{ pack: pack.collection, update: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await foundry.documents.modifyBatch(compendiumOperations);
|
||||||
|
|
||||||
|
if (wasLocked) {await pack.configure({ locked: true })}
|
||||||
|
|
||||||
|
compendiumOperations = [];
|
||||||
|
};
|
||||||
|
warning.update({ pct: 0.8 });
|
||||||
|
|
||||||
|
// Migrating the world setting
|
||||||
|
const defaultAttrs = game.settings.get(__ID__, `actorDefaultAttributes`)?.at(0);
|
||||||
|
if (defaultAttrs) {
|
||||||
|
const itemSchemas = [];
|
||||||
|
for (const [key, attr] of Object.entries(defaultAttrs)) {
|
||||||
|
itemSchemas.push(convertToItem(key, attr));
|
||||||
|
};
|
||||||
|
await game.settings.set(__ID__, `actorDefaultAttributes`, itemSchemas);
|
||||||
|
};
|
||||||
|
warning.update({ pct: 0.9 });
|
||||||
|
|
||||||
|
await foundry.documents.modifyBatch(worldOperations);
|
||||||
|
finishMigrationWarning(warning, `3.0.0`);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleMigratingActor(actor, options) {
|
||||||
|
const operation = {
|
||||||
|
action: `create`,
|
||||||
|
broadcast: true,
|
||||||
|
documentName: `Item`,
|
||||||
|
parent: actor,
|
||||||
|
pack: options.pack,
|
||||||
|
noHook: true,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const attrs = actor.getFlag(__ID__, flag) ?? {};
|
||||||
|
for (const [ key, attr ] of Object.entries(attrs)) {
|
||||||
|
operation.data.push(convertToItem(key, attr));
|
||||||
|
};
|
||||||
|
|
||||||
|
// No items to create, don't queue the operation
|
||||||
|
if (operation.data.length > 0) {
|
||||||
|
if (actor.inCompendium) {
|
||||||
|
compendiumOperations.push(operation);
|
||||||
|
} else {
|
||||||
|
worldOperations.push(operation);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
"system.attr": _del,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function convertToItem(key, attr) {
|
||||||
|
return {
|
||||||
|
name: attr.name,
|
||||||
|
type: `attribute`,
|
||||||
|
system: {
|
||||||
|
key,
|
||||||
|
value: attr.value,
|
||||||
|
max: attr.isRange ? attr.max : null,
|
||||||
|
aboveTheFold: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -71,7 +71,13 @@ export function registerWorldSettings() {
|
||||||
|
|
||||||
game.settings.register(__ID__, `actorDefaultAttributes`, {
|
game.settings.register(__ID__, `actorDefaultAttributes`, {
|
||||||
config: false,
|
config: false,
|
||||||
type: Object,
|
type: Array,
|
||||||
|
scope: `world`,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(__ID__, `migrationVersion`, {
|
||||||
|
config: false,
|
||||||
|
type: String,
|
||||||
scope: `world`,
|
scope: `world`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
5
module/utils/clamp.mjs
Normal file
5
module/utils/clamp.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function clamp(min, ideal, max) {
|
||||||
|
min ??= Number.NEGATIVE_INFINITY;
|
||||||
|
max ??= Number.POSITIVE_INFINITY;
|
||||||
|
return Math.max(min, Math.min(ideal, max));
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* A helper method that converts an arbitrary string into a format that can be
|
* A helper method that converts an arbitrary string into a format
|
||||||
* used as an object key easily.
|
* that can be used as an object key easily.
|
||||||
*
|
*
|
||||||
* @param {string} text The text to convert
|
* @param {string} text The text to convert
|
||||||
* @returns The converted ID
|
* @returns The converted ID
|
||||||
|
|
@ -9,5 +9,26 @@ export function toID(text) {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s+/g, `_`)
|
.replace(/\s+/g, `_`)
|
||||||
.replace(/\W/g, ``);
|
.replace(/\W/g, ``)
|
||||||
|
.replace(/(^_|_$)/);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper method that reports if an arbitrary string is considered a
|
||||||
|
* valid ID for use in the system
|
||||||
|
*
|
||||||
|
* @param {string} text The text to check
|
||||||
|
* @returns Whether or not the text is a valid ID
|
||||||
|
*/
|
||||||
|
export function isValidID(text) {
|
||||||
|
return !(
|
||||||
|
// any uppercase characters
|
||||||
|
text.match(/[A-Z]/)
|
||||||
|
|
||||||
|
// any non-word characters
|
||||||
|
|| text.match(/\W/)
|
||||||
|
|
||||||
|
// any whitespace characters
|
||||||
|
|| text.match(/\s/)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
56
styles/Apps/AttributeItemSheet.css
Normal file
56
styles/Apps/AttributeItemSheet.css
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
.taf.AttributeItemSheet {
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
> .window-content {
|
||||||
|
padding: 0;
|
||||||
|
color: var(--attribute-sheet-colour);
|
||||||
|
background: var(--attribute-sheet-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-header {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--attribute-sheet-divider-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-controls {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(2, auto);
|
||||||
|
gap: 2px 4px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: left;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 2px 8px;
|
||||||
|
margin: 0 8px 8px;
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--attribute-sheet-hint-colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: var(--attribute-sheet-input-colour);
|
||||||
|
background: var(--attribute-sheet-input-background);
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--attribute-sheet-disabled-input-colour);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taf-toggle {
|
||||||
|
--toggle-background: var(--attribute-sheet-input-background);
|
||||||
|
--slider-checked-colour: var(--attribute-sheet-toggle-slider-enabled-colour);
|
||||||
|
--slider-unchecked-colour: var(--attribute-sheet-toggle-slider-disabled-colour);
|
||||||
|
justify-self: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-tab.active {
|
.items-tab.active,
|
||||||
|
.attributes-tab.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -67,7 +68,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list-header {
|
.embedded-list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -75,8 +76,8 @@
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
padding: 6px 6px 4px;
|
padding: 6px 6px 4px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
background: var(--item-list-header-background);
|
background: var(--embedded-list-header-background);
|
||||||
color: var(--item-list-header-colour);
|
color: var(--embedded-list-header-colour);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
@ -84,18 +85,23 @@
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
height: unset;
|
height: unset;
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
background: var(--item-list-header-input-background);
|
background: var(--embedded-list-header-input-background);
|
||||||
color: var(--item-list-header-input-colour);
|
color: var(--embedded-list-header-input-colour);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list {
|
.embedded-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.two-col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
|
@ -172,6 +178,46 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--attribute-background);
|
||||||
|
color: var(--attribute-colour);
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button {
|
||||||
|
background: var(--item-card-header-input-background);
|
||||||
|
color: var(--item-card-header-input-colour);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--item-card-header-disabled-input-colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:nth-child(odd) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
}
|
||||||
|
&:nth-last-child(2):has( + &:nth-child(even)) {
|
||||||
|
border-radius: 0 0 0 6px;
|
||||||
|
}
|
||||||
|
&:last-child:nth-child(even) {
|
||||||
|
border-radius: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
/* Apps */
|
/* Apps */
|
||||||
@import url("./Apps/common.css") layer(apps);
|
@import url("./Apps/common.css") layer(apps);
|
||||||
@import url("./Apps/Ask.css") layer(apps);
|
@import url("./Apps/Ask.css") layer(apps);
|
||||||
|
@import url("./Apps/AttributeItemSheet.css") layer(apps);
|
||||||
@import url("./Apps/AttributeManager.css") layer(apps);
|
@import url("./Apps/AttributeManager.css") layer(apps);
|
||||||
@import url("./Apps/GenericItemSheet.css") layer(apps);
|
@import url("./Apps/GenericItemSheet.css") layer(apps);
|
||||||
@import url("./Apps/PlayerSheet.css") layer(apps);
|
@import url("./Apps/PlayerSheet.css") layer(apps);
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,16 @@
|
||||||
--inventory-input-colour: var(--steel-100);
|
--inventory-input-colour: var(--steel-100);
|
||||||
--inventory-input-disabled-colour: var(--steel-350);
|
--inventory-input-disabled-colour: var(--steel-350);
|
||||||
|
|
||||||
--item-list-header-background: var(--steel-800);
|
--embedded-list-header-background: var(--steel-800);
|
||||||
--item-list-header-colour: var(--steel-100);
|
--embedded-list-header-colour: var(--steel-100);
|
||||||
--item-list-header-input-background: var(--steel-650);
|
--embedded-list-header-input-background: var(--steel-650);
|
||||||
--item-list-header-input-colour: var(--steel-100);
|
--embedded-list-header-input-colour: var(--steel-100);
|
||||||
|
|
||||||
|
--attribute-background: var(--steel-700);
|
||||||
|
--attribute-colour: var(--steel-100);
|
||||||
|
--attribute-input-background: var(--steel-650);
|
||||||
|
--attribute-input-colour: var(--steel-100);
|
||||||
|
--attribute-disabled-input-colour: var(--steel-350);
|
||||||
|
|
||||||
--item-card-background: #1d262f;
|
--item-card-background: #1d262f;
|
||||||
--item-card-colour: var(--steel-100);
|
--item-card-colour: var(--steel-100);
|
||||||
|
|
@ -44,6 +50,17 @@
|
||||||
--item-sheet-description-menu-background: var(--steel-700);
|
--item-sheet-description-menu-background: var(--steel-700);
|
||||||
--item-sheet-description-content-background: var(--steel-650);
|
--item-sheet-description-content-background: var(--steel-650);
|
||||||
|
|
||||||
|
/* Attribute Sheet Variables */
|
||||||
|
--attribute-sheet-colour: var(--item-sheet-colour);
|
||||||
|
--attribute-sheet-background: var(--item-sheet-background);
|
||||||
|
--attribute-sheet-divider-colour: var(--item-sheet-divider);
|
||||||
|
--attribute-sheet-hint-colour: var(--steel-250);
|
||||||
|
--attribute-sheet-input-colour: var(--item-sheet-input-colour);
|
||||||
|
--attribute-sheet-input-background: var(--item-sheet-input-background);
|
||||||
|
--attribute-sheet-disabled-input-colour: var(--steel-350);
|
||||||
|
--attribute-sheet-toggle-slider-enabled-colour: var(--item-sheet-toggle-slider-enabled-colour);
|
||||||
|
--attribute-sheet-toggle-slider-disabled-colour: var(--item-sheet-toggle-slider-disabled-colour);
|
||||||
|
|
||||||
/* Chip Variables */
|
/* Chip Variables */
|
||||||
--chip-colour: #fff7ed;
|
--chip-colour: #fff7ed;
|
||||||
--chip-background: #2b3642;
|
--chip-background: #2b3642;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "taf",
|
"id": "taf",
|
||||||
"title": "Text-Based Actors",
|
"title": "Text-Based Actors",
|
||||||
"description": "An intentionally minimalist system that enables you to play rules-light games without getting in your way!",
|
"description": "An intentionally minimalist system that enables you to play rules-light games without getting in your way!",
|
||||||
"version": "2.6.2",
|
"version": "3.0.0",
|
||||||
"download": "",
|
"download": "",
|
||||||
"manifest": "",
|
"manifest": "",
|
||||||
"url": "https://git.varify.ca/Foundry/taf",
|
"url": "https://git.varify.ca/Foundry/taf",
|
||||||
|
|
@ -45,6 +45,10 @@
|
||||||
"description"
|
"description"
|
||||||
],
|
],
|
||||||
"filePathFields": {}
|
"filePathFields": {}
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"htmlFields": [],
|
||||||
|
"filePathFields": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
11
templates/AttributeItemSheet/header.hbs
Normal file
11
templates/AttributeItemSheet/header.hbs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<header class="sheet-header bordered">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="large"
|
||||||
|
name="name"
|
||||||
|
value="{{item.name}}"
|
||||||
|
title="{{item.name}}"
|
||||||
|
{{disabled (not meta.editable)}}
|
||||||
|
placeholder="{{localize "Name"}}"
|
||||||
|
>
|
||||||
|
</header>
|
||||||
39
templates/AttributeItemSheet/settings.hbs
Normal file
39
templates/AttributeItemSheet/settings.hbs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<div>
|
||||||
|
<div class="property">
|
||||||
|
<label for="{{meta.idp}}-key">
|
||||||
|
{{ localize "taf.misc.key" }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="{{meta.idp}}-key"
|
||||||
|
name="system.key"
|
||||||
|
value="{{system.key}}"
|
||||||
|
>
|
||||||
|
<p class="hint">
|
||||||
|
{{ localize "taf.misc.attribute.key.hint" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="property">
|
||||||
|
<label for="{{meta.idp}}-aboveTheFold">
|
||||||
|
{{ localize "taf.misc.attribute.always-visible" }}
|
||||||
|
</label>
|
||||||
|
<taf-toggle
|
||||||
|
id="{{meta.idp}}-aboveTheFold"
|
||||||
|
name="system.aboveTheFold"
|
||||||
|
{{checked system.aboveTheFold}}
|
||||||
|
></taf-toggle>
|
||||||
|
</div>
|
||||||
|
{{#if (not system.aboveTheFold)}}
|
||||||
|
<div class="property">
|
||||||
|
<label for="{{meta.idp}}-group">
|
||||||
|
{{ localize "taf.misc.item.group" }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="{{meta.idp}}-group"
|
||||||
|
name="system.group"
|
||||||
|
value="{{system.group}}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
37
templates/AttributeItemSheet/value.hbs
Normal file
37
templates/AttributeItemSheet/value.hbs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="value-controls">
|
||||||
|
<label for="{{meta.idp}}-min">
|
||||||
|
{{ localize "taf.misc.attribute.minimum" }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{meta.idp}}-min"
|
||||||
|
name="system.min"
|
||||||
|
value="{{system.min}}"
|
||||||
|
{{#if system.isRange}}placeholder="0"{{/if}}
|
||||||
|
{{disabled (not meta.editable)}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="{{meta.idp}}-value">
|
||||||
|
{{ localize "taf.misc.attribute.current-value" }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{meta.idp}}-value"
|
||||||
|
name="system.value"
|
||||||
|
value="{{system.value}}"
|
||||||
|
min="{{system.min}}"
|
||||||
|
max="{{system.max}}"
|
||||||
|
{{disabled (not meta.editable)}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="{{meta.idp}}-max">
|
||||||
|
{{ localize "taf.misc.attribute.maximum" }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{meta.idp}}-max"
|
||||||
|
name="system.max"
|
||||||
|
value="{{system.max}}"
|
||||||
|
{{disabled (not meta.editable)}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{{#if hasAttributes}}
|
|
||||||
<div class="attributes">
|
|
||||||
{{#each attrs as | attr |}}
|
|
||||||
<fieldset data-attribute="{{ attr.id }}">
|
|
||||||
<legend>
|
|
||||||
{{ attr.name }}
|
|
||||||
</legend>
|
|
||||||
<div class="attr-range">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="attr-range__value"
|
|
||||||
name="{{attr.path}}.value"
|
|
||||||
value="{{attr.value}}"
|
|
||||||
aria-label="{{localize "taf.Apps.PlayerSheet.current-value"}}"
|
|
||||||
data-tooltip="@{{ attr.id }}{{#if attr.isRange}}.value{{/if}}"
|
|
||||||
>
|
|
||||||
{{#if attr.isRange}}
|
|
||||||
<span aria-hidden="true">/</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="attr-range__max"
|
|
||||||
name="{{attr.path}}.max"
|
|
||||||
value="{{attr.max}}"
|
|
||||||
aria-label="{{localize "taf.Apps.PlayerSheet.max-value"}}"
|
|
||||||
data-tooltip="@{{ attr.id }}.max"
|
|
||||||
>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<template />
|
|
||||||
{{/if}}
|
|
||||||
54
templates/PlayerSheet/primary-attributes.hbs
Normal file
54
templates/PlayerSheet/primary-attributes.hbs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{{#if hasAttributes}}
|
||||||
|
<div class="attributes">
|
||||||
|
{{#each attrs as | attr |}}
|
||||||
|
<fieldset
|
||||||
|
data-item-uuid="{{ attr.uuid }}"
|
||||||
|
data-foreign-uuid="{{ attr.uuid }}"
|
||||||
|
>
|
||||||
|
<legend>
|
||||||
|
{{ attr.name }}
|
||||||
|
</legend>
|
||||||
|
<div class="attr-range">
|
||||||
|
{{#if attr.system.isRange }}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{attr.uuid}}-value"
|
||||||
|
class="attr-range__value"
|
||||||
|
data-foreign-name="system.value"
|
||||||
|
value="{{attr.system.value}}"
|
||||||
|
min="{{attr.system.min}}"
|
||||||
|
max="{{attr.system.max}}"
|
||||||
|
aria-label="{{localize "taf.Apps.PlayerSheet.current-value"}}"
|
||||||
|
data-tooltip="@{{ attr.system.key }}.value"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">/</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{attr.uuid}}-max"
|
||||||
|
class="attr-range__max"
|
||||||
|
data-foreign-name="system.max"
|
||||||
|
value="{{attr.system.max}}"
|
||||||
|
min="{{attr.system.min}}"
|
||||||
|
aria-label="{{localize "taf.Apps.PlayerSheet.max-value"}}"
|
||||||
|
data-tooltip="@{{ attr.system.key }}.max"
|
||||||
|
>
|
||||||
|
{{else}}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{attr.uuid}}-value"
|
||||||
|
class="attr-range__value"
|
||||||
|
data-foreign-name="system.value"
|
||||||
|
value="{{attr.system.value}}"
|
||||||
|
min="{{attr.system.min}}"
|
||||||
|
max="{{attr.system.max}}"
|
||||||
|
aria-label="{{localize "taf.Apps.PlayerSheet.current-value"}}"
|
||||||
|
data-tooltip="@{{ attr.system.key }}"
|
||||||
|
>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<template />
|
||||||
|
{{/if}}
|
||||||
17
templates/PlayerSheet/tabs/attributes/attribute.hbs
Normal file
17
templates/PlayerSheet/tabs/attributes/attribute.hbs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<li
|
||||||
|
class="attribute"
|
||||||
|
data-item-uuid="{{uuid}}"
|
||||||
|
>
|
||||||
|
<div class="title grow">
|
||||||
|
<span class="name">{{ name }}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{uuid}}-value"
|
||||||
|
data-foreign-uuid="{{uuid}}"
|
||||||
|
data-foreign-name="system.value"
|
||||||
|
value="{{ system.value }}"
|
||||||
|
min="{{ system.inferredMinimum }}"
|
||||||
|
max="{{ system.max }}"
|
||||||
|
>
|
||||||
|
</li>
|
||||||
20
templates/PlayerSheet/tabs/attributes/lists.hbs
Normal file
20
templates/PlayerSheet/tabs/attributes/lists.hbs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div
|
||||||
|
class="tab attributes-tab {{ifThen tabActive "active" ""}}"
|
||||||
|
data-group="primary"
|
||||||
|
data-tab="attributes"
|
||||||
|
>
|
||||||
|
{{#each attrGroups as |group|}}
|
||||||
|
<section>
|
||||||
|
<div class="embedded-list-header">
|
||||||
|
<h3 class="grow">
|
||||||
|
{{ group.name }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ul class="embedded-list two-col">
|
||||||
|
{{#each group.attrs as |attr|}}
|
||||||
|
{{> (systemFilePath "templates/PlayerSheet/tabs/attributes/attribute.hbs") attr }}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</section>
|
</section>
|
||||||
{{#each itemGroups as | group |}}
|
{{#each itemGroups as | group |}}
|
||||||
<section>
|
<section>
|
||||||
<div class="item-list-header">
|
<div class="embedded-list-header">
|
||||||
{{#if @root.meta.editable}}
|
{{#if @root.meta.editable}}
|
||||||
<button
|
<button
|
||||||
data-action="createEmbeddedItem"
|
data-action="createEmbeddedItem"
|
||||||
|
|
@ -59,9 +59,9 @@
|
||||||
{{ group.weight }}
|
{{ group.weight }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="item-list">
|
<ul class="embedded-list">
|
||||||
{{#each group.items as |item|}}
|
{{#each group.items as |item|}}
|
||||||
{{> (systemFilePath "templates/PlayerSheet/item.hbs") item }}
|
{{> (systemFilePath "templates/PlayerSheet/tabs/items/item.hbs") item }}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue