Attribute Item Subtype #76
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"
|
||||
},
|
||||
"Item": {
|
||||
"generic": "Generic Item"
|
||||
"generic": "Item",
|
||||
"attribute": "Attribute"
|
||||
}
|
||||
},
|
||||
"taf": {
|
||||
|
|
@ -48,7 +49,8 @@
|
|||
"PlayerSheet": "Player Sheet",
|
||||
"SingleModePlayerSheet": "Player Sheet (Always Editing)",
|
||||
"AttributeOnlyPlayerSheet": "Player Sheet (Attributes Only)",
|
||||
"GenericItemSheet": "System Item Sheet"
|
||||
"GenericItemSheet": "System Item Sheet",
|
||||
"AttributeItemSheet": "Attribute Sheet"
|
||||
},
|
||||
"misc": {
|
||||
"Key": "Key",
|
||||
|
|
@ -67,6 +69,15 @@
|
|||
"quantity": "Quantity",
|
||||
"equipped": "Equipped",
|
||||
"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": {
|
||||
|
|
@ -97,6 +108,7 @@
|
|||
"toggle-item-description": "Show/Hide Item Description",
|
||||
"tab-names": {
|
||||
"content": "Content",
|
||||
"attributes": "Attributes",
|
||||
"items": "Items"
|
||||
}
|
||||
},
|
||||
|
|
@ -133,10 +145,15 @@
|
|||
"missing-id": "An ID must be provided",
|
||||
"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}",
|
||||
"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": {
|
||||
"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";
|
||||
|
||||
// Utils
|
||||
import { isValidID, toID } from "./utils/toID.mjs";
|
||||
import { attributeSorter } from "./utils/attributeSort.mjs";
|
||||
import { DialogManager } from "./utils/DialogManager.mjs";
|
||||
import { localizer } from "./utils/localizer.mjs";
|
||||
import { QueryManager } from "./utils/QueryManager.mjs";
|
||||
import { toID } from "./utils/toID.mjs";
|
||||
|
||||
const { deepFreeze } = foundry.utils;
|
||||
|
||||
|
|
@ -26,5 +26,6 @@ export const api = deepFreeze({
|
|||
attributeSorter,
|
||||
localizer,
|
||||
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 { deleteItemFromElement, editItemFromElement } from "./utils.mjs";
|
||||
import { AttributeManager } from "./AttributeManager.mjs";
|
||||
import { attributeSorter } from "../utils/attributeSort.mjs";
|
||||
import { config } from "../config.mjs";
|
||||
import { Logger } from "../utils/Logger.mjs";
|
||||
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
|
||||
|
|
@ -45,14 +44,21 @@ export class PlayerSheet extends
|
|||
|
||||
static PARTS = {
|
||||
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`) },
|
||||
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
|
||||
items: {
|
||||
template: filePath(`templates/PlayerSheet/item-lists.hbs`),
|
||||
attributeTab: {
|
||||
template: filePath(`templates/PlayerSheet/tabs/attributes/lists.hbs`),
|
||||
scrollable: [``],
|
||||
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`,
|
||||
tabs: [
|
||||
{ id: `content` },
|
||||
{ id: `attributes` },
|
||||
{ id: `items` },
|
||||
],
|
||||
},
|
||||
|
|
@ -104,6 +111,10 @@ export class PlayerSheet extends
|
|||
Logger.debug(`Asserting app "${this.id}" from tab "items" to "${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) {
|
||||
case `content`: return this.hasContentTab;
|
||||
case `items`: return this.hasItemsTab;
|
||||
case `attributes`: return this.hasAttributesTab;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
|
@ -126,8 +138,17 @@ export class PlayerSheet extends
|
|||
return true;
|
||||
};
|
||||
|
||||
get hasAttributesTab() {
|
||||
return this.actor.itemTypes
|
||||
.attribute
|
||||
?.filter(attr => !attr.system.aboveTheFold)
|
||||
.length > 0;
|
||||
};
|
||||
|
||||
get hasItemsTab() {
|
||||
return this.actor.items.size > 0;
|
||||
return this.actor.items
|
||||
.filter(item => item.type !== `attribute`)
|
||||
.length > 0;
|
||||
};
|
||||
// #endregion Instance Data
|
||||
|
||||
|
|
@ -207,7 +228,7 @@ export class PlayerSheet extends
|
|||
|
||||
new ContextMenu.implementation(
|
||||
this.element,
|
||||
`li.item`,
|
||||
`[data-item-uuid]`,
|
||||
[
|
||||
{
|
||||
label: _loc(`taf.misc.edit`),
|
||||
|
|
@ -254,8 +275,12 @@ export class PlayerSheet extends
|
|||
|
||||
async _preparePartContext(partID, ctx) {
|
||||
switch (partID) {
|
||||
case `attributes`: {
|
||||
await this._prepareAttributes(ctx);
|
||||
case `primaryAttributes`: {
|
||||
await this._preparePrimaryAttributes(ctx);
|
||||
break;
|
||||
};
|
||||
case `attributeTab`: {
|
||||
await this._prepareAttributesTab(ctx);
|
||||
break;
|
||||
};
|
||||
case `tabs`: {
|
||||
|
|
@ -275,18 +300,35 @@ export class PlayerSheet extends
|
|||
return ctx;
|
||||
};
|
||||
|
||||
async _prepareAttributes(ctx) {
|
||||
ctx.hasAttributes = this.actor.system.hasAttributes;
|
||||
async _preparePrimaryAttributes(ctx) {
|
||||
const attrs = this.actor.itemTypes.attribute ?? [];
|
||||
const filtered = attrs.filter(attr => attr.system.aboveTheFold);
|
||||
ctx.hasAttributes = filtered.length > 0;
|
||||
ctx.attrs = filtered;
|
||||
};
|
||||
|
||||
const attrs = [];
|
||||
for (const [id, data] of Object.entries(this.actor.system.attr)) {
|
||||
attrs.push({
|
||||
...data,
|
||||
id,
|
||||
path: `system.attr.${id}`,
|
||||
async _prepareAttributesTab(ctx) {
|
||||
ctx.tabActive = this.tabGroups.primary === `attributes`;
|
||||
|
||||
const groups = new Map();
|
||||
const attrs = (this.actor.itemTypes.attribute ?? [])
|
||||
.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) {
|
||||
|
|
@ -321,19 +363,24 @@ export class PlayerSheet extends
|
|||
|
||||
ctx.itemGroups = [];
|
||||
for (const [groupName, items] of Object.entries(this.actor.itemTypes)) {
|
||||
|
||||
// We don't care about attribute items here
|
||||
if (groupName === `attribute`) { continue };
|
||||
|
||||
const preparedItems = [];
|
||||
|
||||
let summedWeight = 0;
|
||||
for (const item of items) {
|
||||
summedWeight += item.system.quantifiedWeight;
|
||||
preparedItems.push(await this._prepareItem(item));
|
||||
summedWeight += item.system.quantifiedWeight ?? 0;
|
||||
const data = await this._prepareItem(item);
|
||||
if (data) { preparedItems.push(data) };
|
||||
};
|
||||
totalWeight += summedWeight;
|
||||
|
||||
ctx.itemGroups.push({
|
||||
name: groupName.titleCase(),
|
||||
items: preparedItems,
|
||||
weight: config.weightFormatter(totalWeight),
|
||||
weight: config.weightFormatter(summedWeight),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -343,6 +390,7 @@ export class PlayerSheet extends
|
|||
};
|
||||
|
||||
async _prepareItem(item) {
|
||||
if (item.type !== `generic`) { return };
|
||||
const ctx = {
|
||||
uuid: item.uuid,
|
||||
img: item.img,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { __ID__ } from "../../consts.mjs";
|
||||
|
||||
export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||
// #region Schema
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
|
|
@ -12,24 +15,53 @@ export class PlayerData extends foundry.abstract.TypeDataModel {
|
|||
nullable: true,
|
||||
initial: null,
|
||||
}),
|
||||
attr: new fields.TypedObjectField(
|
||||
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,
|
||||
},
|
||||
),
|
||||
attr: new fields.ObjectField({ persisted: false, initial: {} }),
|
||||
};
|
||||
};
|
||||
// #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() {
|
||||
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 { clamp } from "../utils/clamp.mjs";
|
||||
|
||||
const { Actor } = foundry.documents;
|
||||
const { hasProperty } = foundry.utils;
|
||||
const { deepClone, setProperty } = foundry.utils;
|
||||
|
||||
export class TAFActor extends Actor {
|
||||
|
||||
// #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
|
||||
* 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);
|
||||
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
|
||||
|
||||
// #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) {
|
||||
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 current = isBar ? attr.value : attr;
|
||||
const update = isDelta ? current + value : value;
|
||||
|
|
@ -44,18 +60,7 @@ export class TAFActor extends Actor {
|
|||
return this;
|
||||
};
|
||||
|
||||
// Determine the updates to make to the actor data
|
||||
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;
|
||||
return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
|
||||
};
|
||||
// #endregion Token Attributes
|
||||
|
||||
|
|
@ -88,8 +93,15 @@ export class TAFActor extends Actor {
|
|||
};
|
||||
// #endregion Roll Data
|
||||
|
||||
// #region Getters
|
||||
// #region Methods
|
||||
#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() {
|
||||
if (this.#sortedTypes) { return this.#sortedTypes };
|
||||
const types = {};
|
||||
|
|
@ -105,5 +117,58 @@ export class TAFActor extends Actor {
|
|||
};
|
||||
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) {
|
||||
let editable = hasProperty(system, `${attribute}.value`);
|
||||
const isRange = getProperty(system, `${attribute}.isRange`);
|
||||
if (isRange) {
|
||||
return {
|
||||
type: `bar`,
|
||||
attribute,
|
||||
|
|
@ -92,14 +90,6 @@ export class TAFTokenDocument extends TokenDocument {
|
|||
max: parseInt(data.max || 0),
|
||||
editable,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: `value`,
|
||||
attribute: `${attribute}.value`,
|
||||
value: Number(data.value),
|
||||
editable,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Otherwise null
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
// Apps
|
||||
import { AttributeItemSheet } from "../apps/AttributeItemSheet.mjs";
|
||||
import { AttributeOnlyPlayerSheet } from "../apps/AttributeOnlyPlayerSheet.mjs";
|
||||
import { GenericItemSheet } from "../apps/GenericItemSheet.mjs";
|
||||
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
||||
import { SingleModePlayerSheet } from "../apps/SingleModePlayerSheet.mjs";
|
||||
|
||||
// Data Models
|
||||
import { AttributeItemData } from "../data/Item/attribute.mjs";
|
||||
import { GenericItemData } from "../data/Item/generic.mjs";
|
||||
import { PlayerData } from "../data/Actor/player.mjs";
|
||||
|
||||
|
|
@ -35,6 +37,7 @@ Hooks.on(`init`, () => {
|
|||
// #region Data Models
|
||||
CONFIG.Actor.dataModels.player = PlayerData;
|
||||
CONFIG.Item.dataModels.generic = GenericItemData;
|
||||
CONFIG.Item.dataModels.attribute = AttributeItemData;
|
||||
// #endregion Data Models
|
||||
|
||||
// #region Sheets
|
||||
|
|
@ -61,10 +64,20 @@ Hooks.on(`init`, () => {
|
|||
__ID__,
|
||||
GenericItemSheet,
|
||||
{
|
||||
types: [`generic`],
|
||||
makeDefault: true,
|
||||
label: `taf.sheet-names.GenericItemSheet`,
|
||||
},
|
||||
);
|
||||
foundry.documents.collections.Items.registerSheet(
|
||||
__ID__,
|
||||
AttributeItemSheet,
|
||||
{
|
||||
types: [`attribute`],
|
||||
makeDefault: true,
|
||||
label: `taf.sheet-names.AttributeItemSheet`,
|
||||
},
|
||||
);
|
||||
// #endregion Sheets
|
||||
|
||||
registerWorldSettings();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { checkMigrations } from "../migrations/checkMigrations.mjs";
|
||||
|
||||
Hooks.on(`ready`, () => {
|
||||
// Remove with issue: Foundry/taf#52
|
||||
if (game.release.generation < 14 && globalThis._loc == null) {
|
||||
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`, {
|
||||
config: false,
|
||||
type: Object,
|
||||
type: Array,
|
||||
scope: `world`,
|
||||
});
|
||||
|
||||
game.settings.register(__ID__, `migrationVersion`, {
|
||||
config: false,
|
||||
type: String,
|
||||
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
|
||||
* used as an object key easily.
|
||||
* A helper method that converts an arbitrary string into a format
|
||||
* that can be used as an object key easily.
|
||||
*
|
||||
* @param {string} text The text to convert
|
||||
* @returns The converted ID
|
||||
|
|
@ -9,5 +9,26 @@ export function toID(text) {
|
|||
return text
|
||||
.toLowerCase()
|
||||
.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;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.item-list-header {
|
||||
.embedded-list-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
@ -75,8 +76,8 @@
|
|||
border-radius: 6px 6px 0 0;
|
||||
padding: 6px 6px 4px;
|
||||
margin-bottom: 2px;
|
||||
background: var(--item-list-header-background);
|
||||
color: var(--item-list-header-colour);
|
||||
background: var(--embedded-list-header-background);
|
||||
color: var(--embedded-list-header-colour);
|
||||
|
||||
button {
|
||||
padding: 2px;
|
||||
|
|
@ -84,18 +85,23 @@
|
|||
aspect-ratio: 1;
|
||||
height: unset;
|
||||
min-height: unset;
|
||||
background: var(--item-list-header-input-background);
|
||||
color: var(--item-list-header-input-colour);
|
||||
background: var(--embedded-list-header-input-background);
|
||||
color: var(--embedded-list-header-input-colour);
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
.embedded-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
/* Apps */
|
||||
@import url("./Apps/common.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/GenericItemSheet.css") layer(apps);
|
||||
@import url("./Apps/PlayerSheet.css") layer(apps);
|
||||
|
|
|
|||
|
|
@ -19,10 +19,16 @@
|
|||
--inventory-input-colour: var(--steel-100);
|
||||
--inventory-input-disabled-colour: var(--steel-350);
|
||||
|
||||
--item-list-header-background: var(--steel-800);
|
||||
--item-list-header-colour: var(--steel-100);
|
||||
--item-list-header-input-background: var(--steel-650);
|
||||
--item-list-header-input-colour: var(--steel-100);
|
||||
--embedded-list-header-background: var(--steel-800);
|
||||
--embedded-list-header-colour: var(--steel-100);
|
||||
--embedded-list-header-input-background: var(--steel-650);
|
||||
--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-colour: var(--steel-100);
|
||||
|
|
@ -44,6 +50,17 @@
|
|||
--item-sheet-description-menu-background: var(--steel-700);
|
||||
--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-colour: #fff7ed;
|
||||
--chip-background: #2b3642;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "taf",
|
||||
"title": "Text-Based Actors",
|
||||
"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": "",
|
||||
"manifest": "",
|
||||
"url": "https://git.varify.ca/Foundry/taf",
|
||||
|
|
@ -45,6 +45,10 @@
|
|||
"description"
|
||||
],
|
||||
"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>
|
||||
{{#each itemGroups as | group |}}
|
||||
<section>
|
||||
<div class="item-list-header">
|
||||
<div class="embedded-list-header">
|
||||
{{#if @root.meta.editable}}
|
||||
<button
|
||||
data-action="createEmbeddedItem"
|
||||
|
|
@ -59,9 +59,9 @@
|
|||
{{ group.weight }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="item-list">
|
||||
<ul class="embedded-list">
|
||||
{{#each group.items as |item|}}
|
||||
{{> (systemFilePath "templates/PlayerSheet/item.hbs") item }}
|
||||
{{> (systemFilePath "templates/PlayerSheet/tabs/items/item.hbs") item }}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</section>
|
||||
Loading…
Add table
Add a link
Reference in a new issue