Merge pull request #10 from Oliver-Akins/rewrite/v13
v13 Compatibility and Rewrite
This commit is contained in:
commit
292f8caf92
74 changed files with 833 additions and 1551 deletions
8
.github/workflows/draft-release.yaml
vendored
8
.github/workflows/draft-release.yaml
vendored
|
|
@ -29,10 +29,6 @@ jobs:
|
||||||
if: ${{ steps.check-tag.outputs.exists == 'true' }}
|
if: ${{ steps.check-tag.outputs.exists == 'true' }}
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
- name: Ensure there are specific files to release
|
|
||||||
if: ${{ vars.files_to_release == '' }}
|
|
||||||
run: exit 1
|
|
||||||
|
|
||||||
# Compile the stuff that needs to be compiled
|
# Compile the stuff that needs to be compiled
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: node scripts/buildCompendia.mjs
|
- run: node scripts/buildCompendia.mjs
|
||||||
|
|
@ -43,10 +39,10 @@ jobs:
|
||||||
|
|
||||||
- name: Update the download property in the manifest
|
- name: Update the download property in the manifest
|
||||||
id: manifest-update
|
id: manifest-update
|
||||||
run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/${{ vars.zip_name }}.zip"' > system.json
|
run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"' > system.json
|
||||||
|
|
||||||
- name: Create the zip
|
- name: Create the zip
|
||||||
run: zip -r ${{ vars.zip_name || 'release' }}.zip ${{ vars.files_to_release }}
|
run: zip -r release.zip langs module styles templates system.json README.md
|
||||||
|
|
||||||
- name: Create the draft release
|
- name: Create the draft release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.styles
|
deprecated
|
||||||
|
|
|
||||||
BIN
.promo/hjonk-samples.png
Normal file
BIN
.promo/hjonk-samples.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
21
README.md
21
README.md
|
|
@ -2,24 +2,3 @@
|
||||||
This is an intentionally bare-bones system that features a text-only character
|
This is an intentionally bare-bones system that features a text-only character
|
||||||
sheet, allowing the playing of games that may not otherwise have a Foundry system
|
sheet, allowing the playing of games that may not otherwise have a Foundry system
|
||||||
implementation.
|
implementation.
|
||||||
|
|
||||||
## Features
|
|
||||||
There are not too many features included in this system for things like automation
|
|
||||||
as it's meant to be used to mostly play rules-light games. However there are some
|
|
||||||
special features that can be enabled on a per-world basis using a world script
|
|
||||||
to enable feature flags that you want.
|
|
||||||
|
|
||||||
### List of Feature Flags
|
|
||||||
| Flag | Description
|
|
||||||
| - | -
|
|
||||||
| `ROLL_MODE_CONTENT` | Allows players, GMs, and macros to send "blind" chat messages where only the GM gets to see the content.
|
|
||||||
| `STORABLE_SHEET_SIZE` | Makes it so that certain sheets are able to have their size saved, so that it resumes that size when opened.
|
|
||||||
|
|
||||||
### Example Feature Flag
|
|
||||||
In order to change these flags, you must make a world script that has something
|
|
||||||
like the below code in it:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// v- this is the name of the flag from the table above
|
|
||||||
taf.FEATURES.STORABLE_SHEET_SIZE = true;
|
|
||||||
```
|
|
||||||
|
|
@ -20,10 +20,6 @@ export default [
|
||||||
Handlebars: `readonly`,
|
Handlebars: `readonly`,
|
||||||
Hooks: `readonly`,
|
Hooks: `readonly`,
|
||||||
ui: `readonly`,
|
ui: `readonly`,
|
||||||
Actor: `readonly`,
|
|
||||||
Actors: `readonly`,
|
|
||||||
Item: `readonly`,
|
|
||||||
Items: `readonly`,
|
|
||||||
ActorSheet: `readonly`,
|
ActorSheet: `readonly`,
|
||||||
ItemSheet: `readonly`,
|
ItemSheet: `readonly`,
|
||||||
foundry: `readonly`,
|
foundry: `readonly`,
|
||||||
|
|
@ -31,7 +27,8 @@ export default [
|
||||||
ActiveEffect: `readonly`,
|
ActiveEffect: `readonly`,
|
||||||
Dialog: `readonly`,
|
Dialog: `readonly`,
|
||||||
renderTemplate: `readonly`,
|
renderTemplate: `readonly`,
|
||||||
TextEditor: `readonly`,
|
fromUuid: `readonly`,
|
||||||
|
fromUuidSync: `readonly`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
18
langs/en-ca.json
Normal file
18
langs/en-ca.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"TYPES": {
|
||||||
|
"Actor": {
|
||||||
|
"player": "Player"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taf": {
|
||||||
|
"settings": {
|
||||||
|
"canPlayersManageAttributes": {
|
||||||
|
"name": "Players Can Manage Attributes",
|
||||||
|
"hint": "This allows players who have edit access to a document to be able to edit what attributes those characters have via the attribute editor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sheet-names": {
|
||||||
|
"PlayerSheet": "Player Sheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
module/apps/AttributeManager.mjs
Normal file
171
module/apps/AttributeManager.mjs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { toID } from "../utils/toID.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
const { deepClone, diffObject, randomID, setProperty } = foundry.utils;
|
||||||
|
|
||||||
|
export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: `form`,
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`AttributeManager`,
|
||||||
|
],
|
||||||
|
position: {
|
||||||
|
width: 400,
|
||||||
|
height: 350,
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
submitOnChange: false,
|
||||||
|
closeOnSubmit: true,
|
||||||
|
handler: this.#onSubmit,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addNew: this.#addNew,
|
||||||
|
removeAttribute: this.#remove,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
attributes: {
|
||||||
|
template: filePath(`templates/AttributeManager/attribute-list.hbs`),
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
template: filePath(`templates/AttributeManager/controls.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Instance Data
|
||||||
|
/** @type {string | null} */
|
||||||
|
#doc = null;
|
||||||
|
|
||||||
|
#attributes;
|
||||||
|
|
||||||
|
constructor({ document , ...options } = {}) {
|
||||||
|
super(options);
|
||||||
|
this.#doc = document;
|
||||||
|
this.#attributes = deepClone(document.system.attr);
|
||||||
|
};
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return `Attributes: ${this.#doc.name}`;
|
||||||
|
};
|
||||||
|
// #endregion Instance Data
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _onRender(context, options) {
|
||||||
|
await super._onRender(context, options);
|
||||||
|
|
||||||
|
const elements = this.element
|
||||||
|
.querySelectorAll(`[data-bind]`);
|
||||||
|
for (const input of elements) {
|
||||||
|
input.addEventListener(`change`, this.#bindListener.bind(this));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _preparePartContext(partId) {
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
ctx.actor = this.#doc;
|
||||||
|
|
||||||
|
switch (partId) {
|
||||||
|
case `attributes`: {
|
||||||
|
await this._prepareAttributeContext(ctx);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareAttributeContext(ctx) {
|
||||||
|
const attrs = [];
|
||||||
|
for (const [id, data] of Object.entries(this.#attributes)) {
|
||||||
|
if (data == null) { continue };
|
||||||
|
attrs.push({
|
||||||
|
id,
|
||||||
|
name: data.name,
|
||||||
|
isRange: data.isRange,
|
||||||
|
isNew: data.isNew ?? false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ctx.attrs = attrs;
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
async #bindListener(event) {
|
||||||
|
const target = event.target;
|
||||||
|
const data = target.dataset;
|
||||||
|
const binding = data.bind;
|
||||||
|
|
||||||
|
let value = target.value;
|
||||||
|
switch (target.type) {
|
||||||
|
case `checkbox`: {
|
||||||
|
value = target.checked;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.debug(`Updating ${binding} value to ${value}`);
|
||||||
|
setProperty(this.#attributes, binding, value);
|
||||||
|
await this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #addNew() {
|
||||||
|
const id = randomID();
|
||||||
|
this.#attributes[id] = {
|
||||||
|
name: ``,
|
||||||
|
isRange: false,
|
||||||
|
isNew: true,
|
||||||
|
};
|
||||||
|
await this.render({ parts: [ `attributes` ]});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #remove($e, element) {
|
||||||
|
const attribute = element.closest(`[data-attribute]`)?.dataset.attribute;
|
||||||
|
if (!attribute) { return };
|
||||||
|
delete this.#attributes[attribute];
|
||||||
|
this.#attributes[`-=${attribute}`] = null;
|
||||||
|
await this.render({ parts: [ `attributes` ] });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {AttributeManager} */
|
||||||
|
static async #onSubmit() {
|
||||||
|
const entries = Object.entries(this.#attributes)
|
||||||
|
.map(([id, attr]) => {
|
||||||
|
if (attr == null) {
|
||||||
|
return [ id, attr ];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attr.isNew) {
|
||||||
|
delete attr.isNew;
|
||||||
|
return [ toID(attr.name), attr ];
|
||||||
|
};
|
||||||
|
|
||||||
|
return [ id, attr ];
|
||||||
|
});
|
||||||
|
const data = Object.fromEntries(entries);
|
||||||
|
|
||||||
|
const diff = diffObject(
|
||||||
|
this.#doc.system.attr,
|
||||||
|
data,
|
||||||
|
{ inner: false, deletionKeys: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.#doc.update({ "system.attr": diff });
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
111
module/apps/PlayerSheet.mjs
Normal file
111
module/apps/PlayerSheet.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { AttributeManager } from "./AttributeManager.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||||
|
|
||||||
|
export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
|
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`PlayerSheet`,
|
||||||
|
],
|
||||||
|
position: {
|
||||||
|
width: 575,
|
||||||
|
height: 740,
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
submitOnChange: true,
|
||||||
|
closeOnSubmit: false,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
manageAttributes: this.#manageAttributes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
header: { template: filePath(`templates/PlayerSheet/header.hbs`) },
|
||||||
|
attributes: { template: filePath(`templates/PlayerSheet/attributes.hbs`) },
|
||||||
|
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
_getHeaderControls() {
|
||||||
|
const controls = super._getHeaderControls();
|
||||||
|
|
||||||
|
controls.push({
|
||||||
|
icon: `fa-solid fa-at`,
|
||||||
|
label: `Manage Attributes`,
|
||||||
|
action: `manageAttributes`,
|
||||||
|
visible: () => {
|
||||||
|
const isGM = game.user.isGM;
|
||||||
|
const allowPlayerEdits = game.settings.get(__ID__, `canPlayersManageAttributes`);
|
||||||
|
const editable = this.isEditable;
|
||||||
|
return isGM || (allowPlayerEdits && editable);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return controls;
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _preparePartContext(partID) {
|
||||||
|
let ctx = {
|
||||||
|
actor: this.actor,
|
||||||
|
system: this.actor.system,
|
||||||
|
editable: this.isEditable,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (partID) {
|
||||||
|
case `attributes`: {
|
||||||
|
await this._prepareAttributes(ctx);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
case `content`: {
|
||||||
|
await this._prepareContent(ctx);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareAttributes(ctx) {
|
||||||
|
ctx.hasAttributes = this.actor.system.hasAttributes;
|
||||||
|
|
||||||
|
const attrs = [];
|
||||||
|
for (const [id, data] of Object.entries(this.actor.system.attr)) {
|
||||||
|
attrs.push({
|
||||||
|
...data,
|
||||||
|
id,
|
||||||
|
path: `system.attr.${id}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ctx.attrs = attrs.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareContent(ctx) {
|
||||||
|
const TextEditor = foundry.applications.ux.TextEditor.implementation;
|
||||||
|
ctx.enriched = {
|
||||||
|
system: {
|
||||||
|
content: await TextEditor.enrichHTML(this.actor.system.content),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/** @this {PlayerSheet} */
|
||||||
|
static async #manageAttributes() {
|
||||||
|
const app = new AttributeManager({ document: this.actor });
|
||||||
|
await app.render({ force: true });
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
9
module/consts.mjs
Normal file
9
module/consts.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const __ID__ = `taf`;
|
||||||
|
|
||||||
|
// MARK: filePath
|
||||||
|
export function filePath(path) {
|
||||||
|
if (path.startsWith(`/`)) {
|
||||||
|
path = path.slice(1);
|
||||||
|
};
|
||||||
|
return `systems/${__ID__}/${path}`;
|
||||||
|
};
|
||||||
29
module/data/Player.mjs
Normal file
29
module/data/Player.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
content: new fields.HTMLField({
|
||||||
|
blank: true,
|
||||||
|
trim: true,
|
||||||
|
initial: ``,
|
||||||
|
}),
|
||||||
|
attr: new fields.TypedObjectField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ blank: false, trim: true }),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
get hasAttributes() {
|
||||||
|
return Object.keys(this.attr).length > 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
28
module/documents/Actor.mjs
Normal file
28
module/documents/Actor.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const { Actor } = foundry.documents;
|
||||||
|
|
||||||
|
export class TAFActor extends Actor {
|
||||||
|
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
|
||||||
|
Logger.table({ attribute, value, isDelta, isBar });
|
||||||
|
const attr = foundry.utils.getProperty(this.system, attribute);
|
||||||
|
const current = isBar ? attr.value : attr;
|
||||||
|
const update = isDelta ? current + value : value;
|
||||||
|
if ( update === current ) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
111
module/documents/Token.mjs
Normal file
111
module/documents/Token.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const { TokenDocument } = foundry.documents;
|
||||||
|
const { getProperty, getType, hasProperty, isSubclass } = foundry.utils;
|
||||||
|
|
||||||
|
export class TAFTokenDocument extends TokenDocument {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* This override's purpose is to make it so that Token attributes and bars can
|
||||||
|
* be accessed from the data model's values directly instead of relying on only
|
||||||
|
* the schema, which doesn't account for my TypedObjectField of attributes.
|
||||||
|
*/
|
||||||
|
static getTrackedAttributes(data, _path = []) {
|
||||||
|
|
||||||
|
// Case 1 - Infer attributes from schema structure.
|
||||||
|
if ( (data instanceof foundry.abstract.DataModel) || isSubclass(data, foundry.abstract.DataModel) ) {
|
||||||
|
return this._getTrackedAttributesFromObject(data, _path);
|
||||||
|
}
|
||||||
|
if ( data instanceof foundry.data.fields.SchemaField ) {
|
||||||
|
return this._getTrackedAttributesFromSchema(data, _path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2 - Infer attributes from object structure.
|
||||||
|
if ( [`Object`, `Array`].includes(getType(data)) ) {
|
||||||
|
return this._getTrackedAttributesFromObject(data, _path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3 - Retrieve explicitly configured attributes.
|
||||||
|
if ( !data || (typeof data === `string`) ) {
|
||||||
|
const config = this._getConfiguredTrackedAttributes(data);
|
||||||
|
if ( config ) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
data = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the path and record found attributes
|
||||||
|
if ( data !== undefined ) {
|
||||||
|
return {bar: [], value: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 4 - Infer attributes from system template.
|
||||||
|
const bar = new Set();
|
||||||
|
const value = new Set();
|
||||||
|
for ( const [type, model] of Object.entries(game.model.Actor) ) {
|
||||||
|
const dataModel = CONFIG.Actor.dataModels?.[type];
|
||||||
|
const inner = this.getTrackedAttributes(dataModel ?? model, _path);
|
||||||
|
inner.bar.forEach(attr => bar.add(attr.join(`.`)));
|
||||||
|
inner.value.forEach(attr => value.add(attr.join(`.`)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bar: Array.from(bar).map(attr => attr.split(`.`)),
|
||||||
|
value: Array.from(value).map(attr => attr.split(`.`)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
getBarAttribute(barName, {alternative} = {}) {
|
||||||
|
const attribute = alternative || this[barName]?.attribute;
|
||||||
|
Logger.log(barName, attribute);
|
||||||
|
if (!attribute || !this.actor) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const system = this.actor.system;
|
||||||
|
|
||||||
|
// Get the current attribute value
|
||||||
|
const data = getProperty(system, attribute);
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Number.isNumeric(data)) {
|
||||||
|
let editable = hasProperty(system, attribute);
|
||||||
|
return {
|
||||||
|
type: `value`,
|
||||||
|
attribute,
|
||||||
|
value: Number(data),
|
||||||
|
editable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
value: parseInt(data.value || 0),
|
||||||
|
max: parseInt(data.max || 0),
|
||||||
|
editable,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: `value`,
|
||||||
|
attribute: `${attribute}.value`,
|
||||||
|
value: Number(data.value),
|
||||||
|
editable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Otherwise null
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
36
module/hooks/init.mjs
Normal file
36
module/hooks/init.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Apps
|
||||||
|
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
||||||
|
|
||||||
|
// Data Models
|
||||||
|
import { PlayerData } from "../data/Player.mjs";
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
import { TAFActor } from "../documents/Actor.mjs";
|
||||||
|
import { TAFTokenDocument } from "../documents/Token.mjs";
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
import { registerWorldSettings } from "../settings/world.mjs";
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
Hooks.on(`init`, () => {
|
||||||
|
Logger.debug(`Initializing`);
|
||||||
|
|
||||||
|
CONFIG.Token.documentClass = TAFTokenDocument;
|
||||||
|
CONFIG.Actor.documentClass = TAFActor;
|
||||||
|
|
||||||
|
CONFIG.Actor.dataModels.player = PlayerData;
|
||||||
|
|
||||||
|
foundry.documents.collections.Actors.registerSheet(
|
||||||
|
__ID__,
|
||||||
|
PlayerSheet,
|
||||||
|
{
|
||||||
|
makeDefault: true,
|
||||||
|
label: `taf.sheet-names.PlayerSheet`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
registerWorldSettings();
|
||||||
|
});
|
||||||
1
module/main.mjs
Normal file
1
module/main.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import "./hooks/init.mjs";
|
||||||
12
module/settings/world.mjs
Normal file
12
module/settings/world.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { __ID__ } from "../consts.mjs";
|
||||||
|
|
||||||
|
export function registerWorldSettings() {
|
||||||
|
game.settings.register(__ID__, `canPlayersManageAttributes`, {
|
||||||
|
name: `taf.settings.canPlayersManageAttributes.name`,
|
||||||
|
hint: `taf.settings.canPlayersManageAttributes.hint`,
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
scope: `world`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -12,10 +12,10 @@ const augmentedProps = new Set([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** @type {Console} */
|
/** @type {Console} */
|
||||||
globalThis.Logger = new Proxy(console, {
|
export const Logger = new Proxy(console, {
|
||||||
get(target, prop, _receiver) {
|
get(target, prop, _receiver) {
|
||||||
if (augmentedProps.has(prop)) {
|
if (augmentedProps.has(prop)) {
|
||||||
return (...args) => target[prop](game.system.id, `|`, ...args);
|
return target[prop].bind(target, game.system.id, `|`);
|
||||||
};
|
};
|
||||||
return target[prop];
|
return target[prop];
|
||||||
},
|
},
|
||||||
13
module/utils/toID.mjs
Normal file
13
module/utils/toID.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export function toID(text) {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, `_`)
|
||||||
|
.replace(/\W/g, ``);
|
||||||
|
};
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
async function rollDice() {
|
|
||||||
const sidesOnDice = 6;
|
|
||||||
|
|
||||||
const answers = await DialogManager.ask({
|
|
||||||
id: `eat-the-reich-dice-pool`,
|
|
||||||
question: `Set up your dice pool:`,
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
key: `statBase`,
|
|
||||||
inputType: `number`,
|
|
||||||
defaultValue: 2,
|
|
||||||
label: `Number of Dice`,
|
|
||||||
autofocus: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: `successThreshold`,
|
|
||||||
inputType: `number`,
|
|
||||||
defaultValue: 3,
|
|
||||||
label: `Success Threshold (d${sidesOnDice} > X)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: `critsEnabled`,
|
|
||||||
inputType: `checkbox`,
|
|
||||||
defaultValue: true,
|
|
||||||
label: `Enable Criticals`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const { statBase, successThreshold, critsEnabled } = answers;
|
|
||||||
let rollMode = game.settings.get(`core`, `rollMode`);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let successes = 0;
|
|
||||||
let critsOnly = 0;
|
|
||||||
const results = [];
|
|
||||||
for (let i = statBase; i > 0; i--) {
|
|
||||||
let r = new Roll(`1d${sidesOnDice}`);
|
|
||||||
await r.evaluate();
|
|
||||||
let classes = `roll die d6`;
|
|
||||||
|
|
||||||
// Determine the success count and class modifications for the chat
|
|
||||||
if (r.total > successThreshold) {
|
|
||||||
successes++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
classes += ` failure`
|
|
||||||
}
|
|
||||||
if (r.total === sidesOnDice && critsEnabled) {
|
|
||||||
successes++;
|
|
||||||
critsOnly++;
|
|
||||||
classes += ` success`;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(`<li class="${classes}">${r.total}</li>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = `Rolls:<div class="dice-tooltip"><ol class="dice-rolls">${results.join(``)}</ol></div><hr>Successes: ${successes}<br>Crits: ${critsOnly}`;
|
|
||||||
|
|
||||||
|
|
||||||
if (rollMode === CONST.DICE_ROLL_MODES.BLIND) {
|
|
||||||
ui.notifications.warn(`Cannot make a blind roll from the macro, rolling with mode "Private GM Roll" instead`);
|
|
||||||
rollMode = CONST.DICE_ROLL_MODES.PRIVATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatData = ChatMessage.applyRollMode(
|
|
||||||
{
|
|
||||||
title: `Dice Pool`,
|
|
||||||
content,
|
|
||||||
},
|
|
||||||
rollMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
await ChatMessage.implementation.create(chatData);
|
|
||||||
}
|
|
||||||
|
|
||||||
rollDice()
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { SystemIcon } from "./icon.mjs";
|
|
||||||
import { SystemIncrementer } from "./incrementer.mjs";
|
|
||||||
import { SystemRange } from "./range.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of element classes to register, expects all of them to have a static
|
|
||||||
* property of "elementName" that is the namespaced name that the component will
|
|
||||||
* be registered under. Any elements that are formAssociated have their name added
|
|
||||||
* to the "CONFIG.CACHE.componentListeners" array and should be listened to for
|
|
||||||
* "change" events in sheets.
|
|
||||||
*/
|
|
||||||
const components = [
|
|
||||||
SystemIcon,
|
|
||||||
SystemIncrementer,
|
|
||||||
SystemRange,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function registerCustomComponents() {
|
|
||||||
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
|
||||||
for (const component of components) {
|
|
||||||
if (!window.customElements.get(component.elementName)) {
|
|
||||||
console.debug(`${game.system.id} | Registering component "${component.elementName}"`);
|
|
||||||
window.customElements.define(
|
|
||||||
component.elementName,
|
|
||||||
component,
|
|
||||||
);
|
|
||||||
if (component.formAssociated) {
|
|
||||||
CONFIG.CACHE.componentListeners.push(component.elementName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
Attributes:
|
|
||||||
@property {string} name - The name of the icon, takes precedence over the path
|
|
||||||
@property {string} path - The path of the icon file
|
|
||||||
*/
|
|
||||||
export class SystemIcon extends StyledShadowElement(HTMLElement) {
|
|
||||||
static elementName = `dd-icon`;
|
|
||||||
static formAssociated = false;
|
|
||||||
|
|
||||||
/* Stuff for the mixin to use */
|
|
||||||
static _stylePath = ``;
|
|
||||||
|
|
||||||
|
|
||||||
static _cache = new Map();
|
|
||||||
#container;
|
|
||||||
/** @type {null | string} */
|
|
||||||
_name;
|
|
||||||
/** @type {null | string} */
|
|
||||||
_path;
|
|
||||||
|
|
||||||
/* Stored IDs for all of the hooks that are in this component */
|
|
||||||
#svgHmr;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
|
|
||||||
|
|
||||||
this.#container = document.createElement(`div`);
|
|
||||||
this._shadow.appendChild(this.#container);
|
|
||||||
};
|
|
||||||
|
|
||||||
_mounted = false;
|
|
||||||
async connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
if (this._mounted) { return }
|
|
||||||
|
|
||||||
this._name = this.getAttribute(`name`);
|
|
||||||
this._path = this.getAttribute(`path`);
|
|
||||||
|
|
||||||
/*
|
|
||||||
This converts all of the double-dash prefixed properties on the element to
|
|
||||||
CSS variables so that they don't all need to be provided by doing style=""
|
|
||||||
*/
|
|
||||||
for (const attrVar of this.attributes) {
|
|
||||||
if (attrVar.name?.startsWith(`var:`)) {
|
|
||||||
const prop = attrVar.name.replace(`var:`, ``);
|
|
||||||
this.style.setProperty(`--` + prop, attrVar.value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Try to retrieve the icon if it isn't present, try the path then default to
|
|
||||||
the slot content, as then we can have a default per-icon usage
|
|
||||||
*/
|
|
||||||
let content;
|
|
||||||
if (this._name) {
|
|
||||||
content = await this.#getIcon(`./systems/dotdungeon/assets/${this._name}.svg`);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._path && !content) {
|
|
||||||
content = await this.#getIcon(this._path);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
this.#container.appendChild(content.cloneNode(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is so that when we get an HMR event from Foundry we can appropriately
|
|
||||||
handle it using our logic to update the component and the icon cache.
|
|
||||||
*/
|
|
||||||
if (game.settings.get(game.system.id, `devMode`)) {
|
|
||||||
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
|
|
||||||
if (this._name === iconName || this._path?.endsWith(data.path)) {
|
|
||||||
const svg = this.#parseSVG(data.content);
|
|
||||||
this.constructor._cache.set(iconName, svg);
|
|
||||||
this.#container.replaceChildren(svg.cloneNode(true));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._mounted = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (!this._mounted) { return }
|
|
||||||
|
|
||||||
Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr);
|
|
||||||
|
|
||||||
this._mounted = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
async #getIcon(path) {
|
|
||||||
// Cache hit!
|
|
||||||
if (this.constructor._cache.has(path)) {
|
|
||||||
Logger.debug(`Icon ${path} cache hit`);
|
|
||||||
return this.constructor._cache.get(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const r = await fetch(path);
|
|
||||||
switch (r.status) {
|
|
||||||
case 200:
|
|
||||||
case 201:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.error(`Failed to fetch icon: ${path}`);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.debug(`Adding icon ${path} to the cache`);
|
|
||||||
const svg = this.#parseSVG(await r.text());
|
|
||||||
this.constructor._cache.set(path, svg);
|
|
||||||
return svg;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Takes an SVG string and returns it as a DOM node */
|
|
||||||
#parseSVG(content) {
|
|
||||||
const temp = document.createElement(`div`);
|
|
||||||
temp.innerHTML = content;
|
|
||||||
return temp.querySelector(`svg`);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
|
||||||
import { SystemIcon } from "./icon.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
Attributes:
|
|
||||||
@property {string} name - The path to the value to update
|
|
||||||
@property {number} value - The actual value of the input
|
|
||||||
@property {number} min - The minimum value of the input
|
|
||||||
@property {number} max - The maximum value of the input
|
|
||||||
@property {number?} smallStep - The step size used for the buttons and arrow keys
|
|
||||||
@property {number?} largeStep - The step size used for the buttons + Ctrl and page up / down
|
|
||||||
|
|
||||||
Styling:
|
|
||||||
- `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem)
|
|
||||||
- `--width`: Controls the width of the number input (default 50px)
|
|
||||||
*/
|
|
||||||
export class SystemIncrementer extends StyledShadowElement(HTMLElement) {
|
|
||||||
static elementName = `dd-incrementer`;
|
|
||||||
static formAssociated = true;
|
|
||||||
|
|
||||||
static _stylePath = `v1/components/incrementer.scss`;
|
|
||||||
|
|
||||||
_internals;
|
|
||||||
#input;
|
|
||||||
|
|
||||||
_min;
|
|
||||||
_max;
|
|
||||||
_smallStep;
|
|
||||||
_largeStep;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
// Form internals
|
|
||||||
this._internals = this.attachInternals();
|
|
||||||
this._internals.role = `spinbutton`;
|
|
||||||
};
|
|
||||||
|
|
||||||
get form() {
|
|
||||||
return this._internals.form;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.getAttribute(`name`);
|
|
||||||
}
|
|
||||||
set name(value) {
|
|
||||||
this.setAttribute(`name`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this.getAttribute(`value`);
|
|
||||||
};
|
|
||||||
set value(value) {
|
|
||||||
this.setAttribute(`value`, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return `number`;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.replaceChildren();
|
|
||||||
|
|
||||||
// Attribute parsing / registration
|
|
||||||
const value = this.getAttribute(`value`);
|
|
||||||
this._min = parseInt(this.getAttribute(`min`) ?? 0);
|
|
||||||
this._max = parseInt(this.getAttribute(`max`) ?? 0);
|
|
||||||
this._smallStep = parseInt(this.getAttribute(`smallStep`) ?? 1);
|
|
||||||
this._largeStep = parseInt(this.getAttribute(`largeStep`) ?? 5);
|
|
||||||
|
|
||||||
this._internals.ariaValueMin = this._min;
|
|
||||||
this._internals.ariaValueMax = this._max;
|
|
||||||
|
|
||||||
const container = document.createElement(`div`);
|
|
||||||
|
|
||||||
// The input that the user can see / modify
|
|
||||||
const input = document.createElement(`input`);
|
|
||||||
this.#input = input;
|
|
||||||
input.type = `number`;
|
|
||||||
input.ariaHidden = true;
|
|
||||||
input.min = this.getAttribute(`min`);
|
|
||||||
input.max = this.getAttribute(`max`);
|
|
||||||
input.addEventListener(`change`, this.#updateValue.bind(this));
|
|
||||||
input.value = value;
|
|
||||||
|
|
||||||
// plus button
|
|
||||||
const increment = document.createElement(SystemIcon.elementName);
|
|
||||||
increment.setAttribute(`name`, `ui/plus`);
|
|
||||||
increment.setAttribute(`var:size`, `0.75rem`);
|
|
||||||
increment.setAttribute(`var:fill`, `currentColor`);
|
|
||||||
increment.ariaHidden = true;
|
|
||||||
increment.classList.value = `increment`;
|
|
||||||
increment.addEventListener(`mousedown`, this.#increment.bind(this));
|
|
||||||
|
|
||||||
// minus button
|
|
||||||
const decrement = document.createElement(SystemIcon.elementName);
|
|
||||||
decrement.setAttribute(`name`, `ui/minus`);
|
|
||||||
decrement.setAttribute(`var:size`, `0.75rem`);
|
|
||||||
decrement.setAttribute(`var:fill`, `currentColor`);
|
|
||||||
decrement.ariaHidden = true;
|
|
||||||
decrement.classList.value = `decrement`;
|
|
||||||
decrement.addEventListener(`mousedown`, this.#decrement.bind(this));
|
|
||||||
|
|
||||||
// Construct the DOM
|
|
||||||
container.appendChild(decrement);
|
|
||||||
container.appendChild(input);
|
|
||||||
container.appendChild(increment);
|
|
||||||
this._shadow.appendChild(container);
|
|
||||||
|
|
||||||
/*
|
|
||||||
This converts all of the namespace prefixed properties on the element to
|
|
||||||
CSS variables so that they don't all need to be provided by doing style=""
|
|
||||||
*/
|
|
||||||
for (const attrVar of this.attributes) {
|
|
||||||
if (attrVar.name?.startsWith(`var:`)) {
|
|
||||||
const prop = attrVar.name.replace(`var:`, ``);
|
|
||||||
this.style.setProperty(`--` + prop, attrVar.value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
#updateValue() {
|
|
||||||
let value = parseInt(this.#input.value);
|
|
||||||
if (this.getAttribute(`min`)) {
|
|
||||||
value = Math.max(this._min, value);
|
|
||||||
}
|
|
||||||
if (this.getAttribute(`max`)) {
|
|
||||||
value = Math.min(this._max, value);
|
|
||||||
}
|
|
||||||
this.#input.value = value;
|
|
||||||
this.value = value;
|
|
||||||
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @param {Event} $e */
|
|
||||||
#increment($e) {
|
|
||||||
$e.preventDefault();
|
|
||||||
let value = parseInt(this.#input.value);
|
|
||||||
value += $e.ctrlKey ? this._largeStep : this._smallStep;
|
|
||||||
this.#input.value = value;
|
|
||||||
this.#updateValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @param {Event} $e */
|
|
||||||
#decrement($e) {
|
|
||||||
$e.preventDefault();
|
|
||||||
let value = parseInt(this.#input.value);
|
|
||||||
value -= $e.ctrlKey ? this._largeStep : this._smallStep;
|
|
||||||
this.#input.value = value;
|
|
||||||
this.#updateValue();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
/**
|
|
||||||
* @param {HTMLElement} Base
|
|
||||||
*/
|
|
||||||
export function StyledShadowElement(Base) {
|
|
||||||
return class extends Base {
|
|
||||||
/**
|
|
||||||
* The path to the CSS that is loaded
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
static _stylePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The stringified CSS to use
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
static _styles;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The HTML element of the stylesheet
|
|
||||||
* @type {HTMLStyleElement}
|
|
||||||
*/
|
|
||||||
_style;
|
|
||||||
|
|
||||||
/** @type {ShadowRoot} */
|
|
||||||
_shadow;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hook ID for this element's CSS hot reload
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
#cssHmr;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this._shadow = this.attachShadow({ mode: `open` });
|
|
||||||
this._style = document.createElement(`style`);
|
|
||||||
this._shadow.appendChild(this._style);
|
|
||||||
};
|
|
||||||
|
|
||||||
#mounted = false;
|
|
||||||
connectedCallback() {
|
|
||||||
if (this.#mounted) { return }
|
|
||||||
|
|
||||||
this._getStyles();
|
|
||||||
|
|
||||||
if (game.settings.get(`dotdungeon`, `devMode`)) {
|
|
||||||
this.#cssHmr = Hooks.on(`dd-hmr:css`, (data) => {
|
|
||||||
if (data.path.endsWith(this.constructor._stylePath)) {
|
|
||||||
this._style.innerHTML = data.content;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#mounted = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (!this.#mounted) { return }
|
|
||||||
if (this.#cssHmr != null) {
|
|
||||||
Hooks.off(`dd-hmr:css`, this.#cssHmr);
|
|
||||||
this.#cssHmr = null;
|
|
||||||
};
|
|
||||||
this.#mounted = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
_getStyles() {
|
|
||||||
if (this.constructor._styles) {
|
|
||||||
this._style.innerHTML = this.constructor._styles;
|
|
||||||
} else {
|
|
||||||
fetch(`./systems/dotdungeon/.styles/${this.constructor._stylePath}`)
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => {
|
|
||||||
this.constructor._styles = t;
|
|
||||||
this._style.innerHTML = t;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
Attributes:
|
|
||||||
@property {string} name - The path to the value to update in the datamodel
|
|
||||||
@property {number} value - The actual value of the input
|
|
||||||
@property {number} max - The maximum value that this range has
|
|
||||||
|
|
||||||
@extends {HTMLElement}
|
|
||||||
*/
|
|
||||||
export class SystemRange
|
|
||||||
extends StyledShadowElement(
|
|
||||||
HTMLElement,
|
|
||||||
{ mode: `open`, delegatesFocus: true },
|
|
||||||
) {
|
|
||||||
static elementName = `dd-range`;
|
|
||||||
static formAssociated = true;
|
|
||||||
|
|
||||||
static observedAttributes = [`max`];
|
|
||||||
|
|
||||||
static _stylePath = `v3/components/range.css`;
|
|
||||||
|
|
||||||
_internals;
|
|
||||||
#input;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
// Form internals
|
|
||||||
this._internals = this.attachInternals();
|
|
||||||
this._internals.role = `spinbutton`;
|
|
||||||
};
|
|
||||||
|
|
||||||
get form() {
|
|
||||||
return this._internals.form;
|
|
||||||
};
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.getAttribute(`name`);
|
|
||||||
};
|
|
||||||
set name(value) {
|
|
||||||
this.setAttribute(`name`, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
try {
|
|
||||||
return parseInt(this.getAttribute(`value`));
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
set value(value) {
|
|
||||||
this.setAttribute(`value`, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
get max() {
|
|
||||||
try {
|
|
||||||
return parseInt(this.getAttribute(`max`));
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
set max(value) {
|
|
||||||
this.setAttribute(`max`, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return `number`;
|
|
||||||
};
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
// Attribute validation
|
|
||||||
if (!this.hasAttribute(`max`)) {
|
|
||||||
throw new Error(`dotdungeon | Cannot have a range without a maximum value`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keyboard accessible input for the thing
|
|
||||||
this.#input = document.createElement(`input`);
|
|
||||||
this.#input.type = `number`;
|
|
||||||
this.#input.min = 0;
|
|
||||||
this.#input.max = this.max;
|
|
||||||
this.#input.value = this.value;
|
|
||||||
this.#input.addEventListener(`change`, () => {
|
|
||||||
const inputValue = parseInt(this.#input.value);
|
|
||||||
if (inputValue === this.value) { return };
|
|
||||||
this._updateValue.bind(this)(Math.sign(this.value - inputValue));
|
|
||||||
this._updateValue(Math.sign(this.value - inputValue));
|
|
||||||
});
|
|
||||||
this._shadow.appendChild(this.#input);
|
|
||||||
|
|
||||||
// Shadow-DOM construction
|
|
||||||
this._elements = new Array(this.max);
|
|
||||||
const container = document.createElement(`div`);
|
|
||||||
container.classList.add(`container`);
|
|
||||||
|
|
||||||
// Creating the node for filled content
|
|
||||||
const filledContainer = document.createElement(`div`);
|
|
||||||
filledContainer.classList.add(`range-increment`, `filled`);
|
|
||||||
const filledNode = this.querySelector(`[slot="filled"]`);
|
|
||||||
if (filledNode) { filledContainer.appendChild(filledNode) };
|
|
||||||
|
|
||||||
const emptyContainer = document.createElement(`div`);
|
|
||||||
emptyContainer.classList.add(`range-increment`, `empty`);
|
|
||||||
const emptyNode = this.querySelector(`[slot="empty"]`);
|
|
||||||
if (emptyNode) { emptyContainer.appendChild(emptyNode) };
|
|
||||||
|
|
||||||
this._elements.fill(filledContainer, 0, this.value);
|
|
||||||
this._elements.fill(emptyContainer, this.value);
|
|
||||||
container.append(...this._elements.map((slot, i) => {
|
|
||||||
const node = slot.cloneNode(true);
|
|
||||||
node.setAttribute(`data-index`, i + 1);
|
|
||||||
node.addEventListener(`click`, () => {
|
|
||||||
const filled = node.classList.contains(`filled`);
|
|
||||||
this._updateValue(filled ? -1 : 1);
|
|
||||||
});
|
|
||||||
return node;
|
|
||||||
}));
|
|
||||||
this._shadow.appendChild(container);
|
|
||||||
|
|
||||||
/*
|
|
||||||
This converts all of the namespace prefixed properties on the element to
|
|
||||||
CSS variables so that they don't all need to be provided by doing style=""
|
|
||||||
*/
|
|
||||||
for (const attrVar of this.attributes) {
|
|
||||||
if (attrVar.name?.startsWith(`var:`)) {
|
|
||||||
const prop = attrVar.name.replace(`var:`, ``);
|
|
||||||
this.style.setProperty(`--` + prop, attrVar.value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateValue(delta) {
|
|
||||||
this.value += delta;
|
|
||||||
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object of Foundry-types to in-code Document classes.
|
|
||||||
*/
|
|
||||||
const classes = {};
|
|
||||||
|
|
||||||
/** The class that will be used if no type-specific class is defined */
|
|
||||||
const defaultClass = ActiveEffect;
|
|
||||||
|
|
||||||
export const ActiveEffectProxy = createDocumentProxy(defaultClass, classes);
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export class Player extends Actor {
|
|
||||||
getRollData() {
|
|
||||||
return this.system;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export class PlayerData extends foundry.abstract.TypeDataModel {
|
|
||||||
static defineSchema() {
|
|
||||||
const fields = foundry.data.fields;
|
|
||||||
return {
|
|
||||||
content: new fields.HTMLField({
|
|
||||||
blank: true,
|
|
||||||
trim: true,
|
|
||||||
initial: ``,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object of Foundry-types to in-code Document classes.
|
|
||||||
*/
|
|
||||||
const classes = {};
|
|
||||||
|
|
||||||
/** The class that will be used if no type-specific class is defined */
|
|
||||||
const defaultClass = Actor;
|
|
||||||
|
|
||||||
export const ActorProxy = createDocumentProxy(defaultClass, classes);
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object of Foundry-types to in-code Document classes.
|
|
||||||
*/
|
|
||||||
const classes = {};
|
|
||||||
|
|
||||||
/** The class that will be used if no type-specific class is defined */
|
|
||||||
const defaultClass = ChatMessage;
|
|
||||||
|
|
||||||
export const ChatMessageProxy = createDocumentProxy(defaultClass, classes);
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object of Foundry-types to in-code Document classes.
|
|
||||||
*/
|
|
||||||
const classes = {};
|
|
||||||
|
|
||||||
/** The class that will be used if no type-specific class is defined */
|
|
||||||
const defaultClass = Item;
|
|
||||||
|
|
||||||
export const ItemProxy = createDocumentProxy(defaultClass, classes);
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { handlebarsLocalizer, localizer } from "../utils/localizer.mjs";
|
|
||||||
import { options } from "./options.mjs";
|
|
||||||
|
|
||||||
export function registerHandlebarsHelpers() {
|
|
||||||
const helperPrefix = game.system.id;
|
|
||||||
|
|
||||||
return {
|
|
||||||
// MARK: Complex helpers
|
|
||||||
[`${helperPrefix}-i18n`]: handlebarsLocalizer,
|
|
||||||
[`${helperPrefix}-options`]: options,
|
|
||||||
|
|
||||||
// MARK: Simple helpers
|
|
||||||
[`${helperPrefix}-stringify`]: v => JSON.stringify(v, null, ` `),
|
|
||||||
[`${helperPrefix}-empty`]: v => v.length == 0,
|
|
||||||
[`${helperPrefix}-set-has`]: (s, k) => s.has(k),
|
|
||||||
[`${helperPrefix}-empty-state`]: (v) => v ?? localizer(`${game.system.id}.common.empty`),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { localizer } from "../utils/localizer.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} Option
|
|
||||||
* @property {string} [label]
|
|
||||||
* @property {string|number} value
|
|
||||||
* @property {boolean} [disabled]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string | number} selected
|
|
||||||
* @param {Array<Option | string>} opts
|
|
||||||
*/
|
|
||||||
export function options(selected, opts, meta) {
|
|
||||||
const { localize = false } = meta.hash;
|
|
||||||
selected = Handlebars.escapeExpression(selected);
|
|
||||||
const htmlOptions = [];
|
|
||||||
|
|
||||||
for (let opt of opts) {
|
|
||||||
if (foundry.utils.getType(opt) === `string`) {
|
|
||||||
opt = { label: opt, value: opt };
|
|
||||||
};
|
|
||||||
opt.value = Handlebars.escapeExpression(opt.value);
|
|
||||||
htmlOptions.push(
|
|
||||||
`<option
|
|
||||||
value="${opt.value}"
|
|
||||||
${selected === opt.value ? `selected` : ``}
|
|
||||||
${opt.disabled ? `disabled` : ``}
|
|
||||||
>
|
|
||||||
${localize ? localizer(opt.label) : opt.label}
|
|
||||||
</option>`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return htmlOptions.join(`\n`);
|
|
||||||
};
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
const loaders = {
|
|
||||||
svg(data) {
|
|
||||||
const iconName = data.path.split(`/`).slice(-1)[0].slice(0, -4);
|
|
||||||
Logger.debug(`hot-reloading icon: ${iconName}`);
|
|
||||||
Hooks.call(`${game.system.id}-hmr:svg`, iconName, data);
|
|
||||||
},
|
|
||||||
js() {window.location.reload()},
|
|
||||||
mjs() {window.location.reload()},
|
|
||||||
css(data) {
|
|
||||||
Logger.debug(`Hot-reloading CSS: ${data.path}`);
|
|
||||||
Hooks.call(`${game.system.id}-hmr:css`, data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Hooks.on(`hotReload`, async (data) => {
|
|
||||||
if (!loaders[data.extension]) {return}
|
|
||||||
return loaders[data.extension](data);
|
|
||||||
});
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
Hooks.on(`renderChatMessage`, (msg, html) => {
|
|
||||||
|
|
||||||
// Short-Circuit when the flag isn't set for the message
|
|
||||||
if (msg.getFlag(`taf`, `rollModedContent`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const featureFlagEnabled = taf.FEATURES.ROLL_MODE_CONTENT;
|
|
||||||
|
|
||||||
const contentElement = html.find(`.message-content`)[0];
|
|
||||||
let content = contentElement.innerHTML;
|
|
||||||
if (featureFlagEnabled && msg.blind && !game.user.isGM) {
|
|
||||||
content = content.replace(/-=.*?=-/gm, `??`);
|
|
||||||
} else {
|
|
||||||
content = content.replace(/-=|=-/gm, ``);
|
|
||||||
}
|
|
||||||
contentElement.innerHTML = content;
|
|
||||||
});
|
|
||||||
64
src/main.mjs
64
src/main.mjs
|
|
@ -1,64 +0,0 @@
|
||||||
// Document Imports
|
|
||||||
import { ActiveEffectProxy } from "./documents/ActiveEffect/_proxy.mjs";
|
|
||||||
import { ActorProxy } from "./documents/Actor/_proxy.mjs";
|
|
||||||
import { ChatMessageProxy } from "./documents/ChatMessage/_proxy.mjs";
|
|
||||||
import { ItemProxy } from "./documents/Item/_proxy.mjs";
|
|
||||||
|
|
||||||
// DataModel Imports
|
|
||||||
import { PlayerData } from "./documents/Actor/Player/Model.mjs";
|
|
||||||
|
|
||||||
// Hook Imports
|
|
||||||
import "./hooks/renderChatMessage.mjs";
|
|
||||||
import "./hooks/hotReload.mjs";
|
|
||||||
|
|
||||||
// Misc Imports
|
|
||||||
import "./utils/globalTaf.mjs";
|
|
||||||
import "./utils/logger.mjs";
|
|
||||||
import "./utils/DialogManager.mjs";
|
|
||||||
import { registerCustomComponents } from "./components/_index.mjs";
|
|
||||||
import { registerHandlebarsHelpers } from "./helpers/_index.mjs";
|
|
||||||
import { registerSettings } from "./settings/_index.mjs";
|
|
||||||
import { registerSheets } from "./sheets/_index.mjs";
|
|
||||||
|
|
||||||
// MARK: init hook
|
|
||||||
Hooks.once(`init`, () => {
|
|
||||||
Logger.info(`Initializing`);
|
|
||||||
CONFIG.ActiveEffect.legacyTransferral = false;
|
|
||||||
|
|
||||||
registerSettings();
|
|
||||||
|
|
||||||
// Data Models
|
|
||||||
CONFIG.Actor.dataModels.player = PlayerData;
|
|
||||||
|
|
||||||
// Update document classes
|
|
||||||
CONFIG.Actor.documentClass = ActorProxy;
|
|
||||||
CONFIG.Item.documentClass = ItemProxy;
|
|
||||||
CONFIG.ActiveEffect.documentClass = ActiveEffectProxy;
|
|
||||||
CONFIG.ChatMessage.documentClass = ChatMessageProxy;
|
|
||||||
registerSheets();
|
|
||||||
|
|
||||||
registerHandlebarsHelpers();
|
|
||||||
|
|
||||||
registerCustomComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: ready hook
|
|
||||||
Hooks.once(`ready`, () => {
|
|
||||||
Logger.info(`Ready`);
|
|
||||||
|
|
||||||
let defaultTab = game.settings.get(game.system.id, `defaultTab`);
|
|
||||||
if (defaultTab) {
|
|
||||||
if (!ui.sidebar?.tabs?.[defaultTab]) {
|
|
||||||
Logger.error(`Couldn't find a sidebar tab with ID:`, defaultTab);
|
|
||||||
} else {
|
|
||||||
Logger.debug(`Switching sidebar tab to:`, defaultTab);
|
|
||||||
ui.sidebar.tabs[defaultTab].activate();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (game.settings.get(game.system.id, `devMode`)) {
|
|
||||||
console.log(`%cFeature Flags:`, `color: #00aa00; font-style: bold; font-size: 1.5rem;`);
|
|
||||||
Logger.table(taf.FEATURES);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { registerClientSettings } from "./client_settings.mjs";
|
|
||||||
import { registerDevSettings } from "./dev_settings.mjs";
|
|
||||||
import { registerWorldSettings } from "./world_settings.mjs";
|
|
||||||
|
|
||||||
export function registerSettings() {
|
|
||||||
Logger.debug(`Registering settings`);
|
|
||||||
registerClientSettings();
|
|
||||||
registerWorldSettings();
|
|
||||||
registerDevSettings();
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export function registerClientSettings() {
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
export function registerDevSettings() {
|
|
||||||
const isLocalhost = window.location.hostname === `localhost`;
|
|
||||||
|
|
||||||
game.settings.register(game.system.id, `devMode`, {
|
|
||||||
name: `Dev Mode?`,
|
|
||||||
scope: `client`,
|
|
||||||
type: Boolean,
|
|
||||||
config: isLocalhost,
|
|
||||||
default: false,
|
|
||||||
requiresReload: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(game.system.id, `defaultTab`, {
|
|
||||||
name: `Default Sidebar Tab`,
|
|
||||||
scope: `client`,
|
|
||||||
type: String,
|
|
||||||
config: isLocalhost,
|
|
||||||
requiresReload: false,
|
|
||||||
onChange(value) {
|
|
||||||
if (!ui.sidebar.tabs[value]) {
|
|
||||||
ui.notifications.warn(`"${value}" cannot be found in the sidebar tabs, it may not work at reload.`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export function registerWorldSettings() {
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { SizeStorable } from "../mixins/SizeStorable.mjs";
|
|
||||||
|
|
||||||
export class PlayerSheetv1 extends SizeStorable(ActorSheet) {
|
|
||||||
static get defaultOptions() {
|
|
||||||
let opts = foundry.utils.mergeObject(
|
|
||||||
super.defaultOptions,
|
|
||||||
{
|
|
||||||
template: `systems/${game.system.id}/templates/Player/v1/main.hbs`,
|
|
||||||
classes: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
opts.classes = [`actor--player`, `style-v1`];
|
|
||||||
return opts;
|
|
||||||
};
|
|
||||||
|
|
||||||
async getData() {
|
|
||||||
const ctx = {};
|
|
||||||
|
|
||||||
ctx.editable = this.isEditable;
|
|
||||||
|
|
||||||
const actor = ctx.actor = this.actor;
|
|
||||||
ctx.system = actor.system;
|
|
||||||
ctx.enriched = { system: {} };
|
|
||||||
ctx.enriched.system.content = await TextEditor.enrichHTML(actor.system.content);
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { PlayerSheetv1 } from "./Player/v1.mjs";
|
|
||||||
|
|
||||||
export function registerSheets() {
|
|
||||||
Logger.debug(`Registering sheets`);
|
|
||||||
|
|
||||||
Actors.registerSheet(game.system.id, PlayerSheetv1, {
|
|
||||||
makeDefault: true,
|
|
||||||
types: [`player`],
|
|
||||||
label: `Hello`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { DialogManager } from "../../utils/DialogManager.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin allows making a class so that it can store the width/height data
|
|
||||||
* to the sheet or localhost in order to make using the text sheets a lil nicer.
|
|
||||||
*
|
|
||||||
* @param {ActorSheet|ItemSheet} cls The Sheet class to augment
|
|
||||||
* @returns The augmented class
|
|
||||||
*/
|
|
||||||
export function SizeStorable(cls) {
|
|
||||||
|
|
||||||
// Don't augment class when the feature isn't enabled
|
|
||||||
if (!taf.FEATURES.STORABLE_SHEET_SIZE) {
|
|
||||||
return cls;
|
|
||||||
}
|
|
||||||
|
|
||||||
return class SizeStorableClass extends cls {
|
|
||||||
constructor(doc, opts) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
Find the saved size of the sheet, it takes the following order of precedence
|
|
||||||
from highest to lowest:
|
|
||||||
- Locally saved
|
|
||||||
- Default values on actor
|
|
||||||
- Default values from constructor
|
|
||||||
*/
|
|
||||||
/** @type {string|undefined} */
|
|
||||||
let size = localStorage.getItem(`${game.system.id}.size:${doc.uuid}`);
|
|
||||||
size ??= doc.getFlag(game.system.id, `size`);
|
|
||||||
|
|
||||||
// Apply the saved value to the options
|
|
||||||
if (size) {
|
|
||||||
const [ width, height ] = size.split(`,`);
|
|
||||||
opts.width = width;
|
|
||||||
opts.height = height;
|
|
||||||
};
|
|
||||||
|
|
||||||
super(doc, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
get hasLocalSize() {
|
|
||||||
return localStorage.getItem(`${game.system.id}.size:${this.object.uuid}`) != null;
|
|
||||||
};
|
|
||||||
|
|
||||||
get hasGlobalSize() {
|
|
||||||
return this.object.getFlag(game.system.id, `size`) != null;
|
|
||||||
};
|
|
||||||
|
|
||||||
_getHeaderButtons() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
class: `size-save`,
|
|
||||||
icon: `fa-solid fa-floppy-disk`,
|
|
||||||
label: `Save Size`,
|
|
||||||
onclick: () => {
|
|
||||||
|
|
||||||
const buttons = {
|
|
||||||
saveGlobal: {
|
|
||||||
label: `Save Global Size`,
|
|
||||||
callback: () => {
|
|
||||||
this.object.setFlag(
|
|
||||||
game.system.id,
|
|
||||||
`size`,
|
|
||||||
`${this.position.width},${this.position.height}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
saveLocal: {
|
|
||||||
label: `Save For Me Only`,
|
|
||||||
callback: () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
`${game.system.id}.size:${this.object.uuid}`,
|
|
||||||
`${this.position.width},${this.position.height}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add resets if there is a size already
|
|
||||||
if (this.hasGlobalSize) {
|
|
||||||
buttons.resetGlobal = {
|
|
||||||
label: `Reset Global Size`,
|
|
||||||
callback: () => {
|
|
||||||
this.object.unsetFlag(game.system.id, `size`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.hasLocalSize) {
|
|
||||||
buttons.resetLocal = {
|
|
||||||
label: `Reset Size For Me Only`,
|
|
||||||
callback: () => {
|
|
||||||
localStorage.removeItem(`${game.system.id}.size:${this.object.uuid}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// When a non-GM is using this system, we only want to save local sizes
|
|
||||||
if (!game.user.isGM) {
|
|
||||||
delete buttons.saveGlobal;
|
|
||||||
delete buttons.resetGlobal;
|
|
||||||
};
|
|
||||||
|
|
||||||
DialogManager.createOrFocus(
|
|
||||||
`${this.object.uuid}:size-save`,
|
|
||||||
{
|
|
||||||
title: `Save size of sheet: ${this.title}`,
|
|
||||||
content: `Saving the size of this sheet will cause it to open at the size it is when you press the save button`,
|
|
||||||
buttons,
|
|
||||||
render: (html) => {
|
|
||||||
const el = html[2];
|
|
||||||
el.style = `display: grid; grid-template-columns: 1fr 1fr; gap: 8px;`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
jQuery: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...super._getHeaderButtons(),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
import { localizer } from "./localizer.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility class that allows managing Dialogs that are created for various
|
|
||||||
* purposes such as deleting items, help popups, etc. This is a singleton class
|
|
||||||
* that upon instantiating after the first time will just return the first instance
|
|
||||||
*/
|
|
||||||
export class DialogManager {
|
|
||||||
|
|
||||||
/** @type {Map<string, Dialog>} */
|
|
||||||
static #dialogs = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses a dialog if it already exists, or creates a new one and renders it.
|
|
||||||
*
|
|
||||||
* @param {string} dialogId The ID to associate with the dialog, should be unique
|
|
||||||
* @param {object} data The data to pass to the Dialog constructor
|
|
||||||
* @param {DialogOptions} opts The options to pass to the Dialog constructor
|
|
||||||
* @returns {Dialog} The Dialog instance
|
|
||||||
*/
|
|
||||||
static async createOrFocus(dialogId, data, opts = {}) {
|
|
||||||
if (DialogManager.#dialogs.has(dialogId)) {
|
|
||||||
const dialog = DialogManager.#dialogs.get(dialogId);
|
|
||||||
dialog.bringToTop();
|
|
||||||
return dialog;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
This makes sure that if I provide a close function as a part of the data,
|
|
||||||
that the dialog still gets removed from the set once it's closed, otherwise
|
|
||||||
it could lead to dangling references that I don't care to keep. Or if I don't
|
|
||||||
provide the close function, it just sets the function as there isn't anything
|
|
||||||
extra that's needed to be called.
|
|
||||||
*/
|
|
||||||
if (data?.close) {
|
|
||||||
const provided = data.close;
|
|
||||||
data.close = () => {
|
|
||||||
DialogManager.#dialogs.delete(dialogId);
|
|
||||||
provided();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
data.close = () => DialogManager.#dialogs.delete(dialogId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the Dialog with the modified data
|
|
||||||
const dialog = new Dialog(data, opts);
|
|
||||||
DialogManager.#dialogs.set(dialogId, dialog);
|
|
||||||
dialog.render(true);
|
|
||||||
return dialog;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes a dialog if it is rendered
|
|
||||||
*
|
|
||||||
* @param {string} dialogId The ID of the dialog to close
|
|
||||||
*/
|
|
||||||
static async close(dialogId) {
|
|
||||||
const dialog = DialogManager.#dialogs.get(dialogId);
|
|
||||||
dialog?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
static async helpDialog(
|
|
||||||
helpId,
|
|
||||||
helpContent,
|
|
||||||
helpTitle = `dotdungeon.common.help`,
|
|
||||||
localizationData = {},
|
|
||||||
) {
|
|
||||||
DialogManager.createOrFocus(
|
|
||||||
helpId,
|
|
||||||
{
|
|
||||||
title: localizer(helpTitle, localizationData),
|
|
||||||
content: localizer(helpContent, localizationData),
|
|
||||||
buttons: {},
|
|
||||||
},
|
|
||||||
{ resizable: true },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user to provide a simple piece of information, this is primarily
|
|
||||||
* intended to be used within macros so that it can have better info gathering
|
|
||||||
* as needed. This returns an object of input keys/labels to the value the user
|
|
||||||
* input for that label, if there is only one input, this will return the value
|
|
||||||
* without an object wrapper, allowing for easier access.
|
|
||||||
*/
|
|
||||||
static async ask(data, opts = {}) {
|
|
||||||
if (!data.id) {
|
|
||||||
throw new Error(`Asking the user for input must contain an ID`);
|
|
||||||
}
|
|
||||||
if (!data.inputs.length) {
|
|
||||||
throw new Error(`Must include at least one input specification when prompting the user`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let autofocusClaimed = false;
|
|
||||||
for (const i of data.inputs) {
|
|
||||||
i.id ??= foundry.utils.randomID(16);
|
|
||||||
i.inputType ??= `text`;
|
|
||||||
|
|
||||||
// Only ever allow one input to claim autofocus
|
|
||||||
i.autofocus &&= !autofocusClaimed;
|
|
||||||
autofocusClaimed ||= i.autofocus;
|
|
||||||
|
|
||||||
// Set the value's attribute name if it isn't specified explicitly
|
|
||||||
if (!i.valueAttribute) {
|
|
||||||
switch (i.inputType) {
|
|
||||||
case `checkbox`:
|
|
||||||
i.valueAttribute = `checked`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
i.valueAttribute = `value`;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
opts.jQuery = true;
|
|
||||||
data.default ??= `confirm`;
|
|
||||||
data.title ??= `System Question`;
|
|
||||||
|
|
||||||
data.content = await renderTemplate(
|
|
||||||
`systems/${game.system.id}/templates/Dialogs/ask.hbs`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
DialogManager.createOrFocus(
|
|
||||||
data.id,
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
buttons: {
|
|
||||||
confirm: {
|
|
||||||
label: `Confirm`,
|
|
||||||
callback: (html) => {
|
|
||||||
const answers = {};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Retrieve the answer for every input provided using the ID
|
|
||||||
determined during initial data prep, and assign the value
|
|
||||||
to the property of the label in the object.
|
|
||||||
*/
|
|
||||||
for (const i of data.inputs) {
|
|
||||||
const element = html.find(`#${i.id}`)[0];
|
|
||||||
let value = element.value;
|
|
||||||
switch (i.inputType) {
|
|
||||||
case `number`:
|
|
||||||
value = parseFloat(value);
|
|
||||||
break;
|
|
||||||
case `checkbox`:
|
|
||||||
value = element.checked;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Logger.debug(`Ask response: ${value} (type: ${typeof value})`);
|
|
||||||
answers[i.key ?? i.label] = value;
|
|
||||||
if (data.inputs.length === 1) {
|
|
||||||
resolve(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(answers);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
label: `Cancel`,
|
|
||||||
callback: () => reject(`User cancelled the prompt`),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
opts,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
static get size() {
|
|
||||||
return DialogManager.#dialogs.size;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
globalThis.DialogManager = DialogManager;
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
export function createDocumentProxy(defaultClass, classes) {
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
return new Proxy(function () {}, {
|
|
||||||
construct(_target, args) {
|
|
||||||
const [data] = args;
|
|
||||||
|
|
||||||
if (!classes[data.type]) {
|
|
||||||
return new defaultClass(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new classes[data.type](...args);
|
|
||||||
},
|
|
||||||
get(_target, prop, _receiver) {
|
|
||||||
|
|
||||||
if ([`create`, `createDocuments`].includes(prop)) {
|
|
||||||
return (data, options) => {
|
|
||||||
if (data.constructor === Array) {
|
|
||||||
return data.map(i => this.constructor.create(i, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!classes[data.type]) {
|
|
||||||
return defaultClass.create(data, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes[data.type].create(data, options);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (prop == Symbol.hasInstance) {
|
|
||||||
return (instance) => {
|
|
||||||
if (instance instanceof defaultClass) {return true}
|
|
||||||
return Object.values(classes).some(i => instance instanceof i);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return defaultClass[prop];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export function hideMessageText(content) {
|
|
||||||
const hideContent = taf.FEATURES.ROLL_MODE_CONTENT;
|
|
||||||
if (hideContent) {
|
|
||||||
return `-=${content}=-`;
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { hideMessageText } from "./feature_flags/rollModeMessageContent.mjs";
|
|
||||||
|
|
||||||
Object.defineProperty(
|
|
||||||
globalThis,
|
|
||||||
`taf`,
|
|
||||||
{
|
|
||||||
value: Object.freeze({
|
|
||||||
utils: Object.freeze({
|
|
||||||
hideMessageText,
|
|
||||||
}),
|
|
||||||
FEATURES: Object.preventExtensions({
|
|
||||||
ROLL_MODE_CONTENT: false,
|
|
||||||
STORABLE_SHEET_SIZE: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
writable: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/** A handlebars helper that utilizes the recursive localizer */
|
|
||||||
export function handlebarsLocalizer(key, ...args) {
|
|
||||||
let data = args[0];
|
|
||||||
if (args.length === 1) { data = args[0].hash }
|
|
||||||
if (key instanceof Handlebars.SafeString) {key = key.toString()}
|
|
||||||
const localized = localizer(key, data);
|
|
||||||
return localized;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A localizer that allows recursively localizing strings so that localized strings
|
|
||||||
* that want to use other localized strings can.
|
|
||||||
*
|
|
||||||
* @param {string} key The localization key to retrieve
|
|
||||||
* @param {object?} args The arguments provided to the localizer for replacement
|
|
||||||
* @param {number?} depth The current depth of the localizer
|
|
||||||
* @returns The localized string
|
|
||||||
*/
|
|
||||||
export function localizer(key, args = {}, depth = 0) {
|
|
||||||
/** @type {string} */
|
|
||||||
let localized = game.i18n.format(key, args);
|
|
||||||
const subkeys = localized.matchAll(/@(?<key>[a-zA-Z.]+)/gm);
|
|
||||||
|
|
||||||
// Short-cut to help prevent infinite recursion
|
|
||||||
if (depth > 10) {
|
|
||||||
return localized;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Helps prevent localization on the same key so that we aren't doing excess work.
|
|
||||||
*/
|
|
||||||
const localizedSubkeys = new Map();
|
|
||||||
for (const match of subkeys) {
|
|
||||||
const subkey = match.groups.key;
|
|
||||||
if (localizedSubkeys.has(subkey)) {continue}
|
|
||||||
localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
return localized.replace(
|
|
||||||
/@(?<key>[a-zA-Z.]+)/gm,
|
|
||||||
(_fullMatch, subkey) => {
|
|
||||||
return localizedSubkeys.get(subkey);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
33
styles/Apps/AttributeManager.css
Normal file
33
styles/Apps/AttributeManager.css
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
.taf.AttributeManager {
|
||||||
|
.attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rebeccapurple;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
styles/Apps/PlayerSheet.css
Normal file
57
styles/Apps/PlayerSheet.css
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
.taf.PlayerSheet {
|
||||||
|
.sheet-header, fieldset, .content {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rebeccapurple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attr-range {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
--table-row-color-odd: var(--table-header-bg-color);
|
||||||
|
|
||||||
|
&:not(:has(> prose-mirror)) {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prose-mirror {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
menu {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
styles/Apps/common.css
Normal file
8
styles/Apps/common.css
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.taf {
|
||||||
|
> .window-content {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
styles/elements/headers.css
Normal file
5
styles/elements/headers.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.taf > .window-content {
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
styles/elements/input.css
Normal file
6
styles/elements/input.css
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.taf > .window-content input {
|
||||||
|
&.large {
|
||||||
|
--input-height: 2.5rem;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
styles/elements/p.css
Normal file
9
styles/elements/p.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.taf > .window-content p {
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
styles/elements/prose-mirror.css
Normal file
12
styles/elements/prose-mirror.css
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.taf > .window-content prose-mirror {
|
||||||
|
background: var(--prosemirror-background);
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
padding: 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper th,
|
||||||
|
.tableWrapper td {
|
||||||
|
border-color: rebeccapurple;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
styles/main.css
Normal file
16
styles/main.css
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
@layer resets, themes, elements, components, partials, apps, exceptions;
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
@import url("./themes/dark.css") layer(themes);
|
||||||
|
@import url("./themes/light.css") layer(themes);
|
||||||
|
|
||||||
|
/* Elements */
|
||||||
|
@import url("./elements/headers.css") layer(elements);
|
||||||
|
@import url("./elements/input.css") layer(elements);
|
||||||
|
@import url("./elements/p.css") layer(elements);
|
||||||
|
@import url("./elements/prose-mirror.css") layer(elements);
|
||||||
|
|
||||||
|
/* Apps */
|
||||||
|
@import url("./Apps/common.css") layer(apps);
|
||||||
|
@import url("./Apps/PlayerSheet.css") layer(apps);
|
||||||
|
@import url("./Apps/AttributeManager.css") layer(apps);
|
||||||
5
styles/resets/inputs.css
Normal file
5
styles/resets/inputs.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.taf > .window-content {
|
||||||
|
button, input {
|
||||||
|
all: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
@use "./v1/index.scss";
|
|
||||||
3
styles/themes/dark.css
Normal file
3
styles/themes/dark.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.theme-dark {
|
||||||
|
--prosemirror-background: var(--color-cool-5);
|
||||||
|
}
|
||||||
3
styles/themes/light.css
Normal file
3
styles/themes/light.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.theme-light {
|
||||||
|
--prosemirror-background: white;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
.dialog-content:not(:only-child) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.prompt {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
// Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
|
||||||
// the corresponding web component. This should only be imported by web component
|
|
||||||
// style files.
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
|
||||||
the corresponding web component. Importing this into other files is forbidden
|
|
||||||
*/
|
|
||||||
|
|
||||||
$default-size: 1rem;
|
|
||||||
|
|
||||||
@use "./common.scss";
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: var(--size, $default-size);
|
|
||||||
height: var(--size, $default-size);
|
|
||||||
fill: var(--fill);
|
|
||||||
stroke: var(--stroke);
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
|
||||||
the corresponding web component. Importing this into other files is forbidden
|
|
||||||
*/
|
|
||||||
|
|
||||||
$default-border-radius: 4px;
|
|
||||||
$default-height: 1.5rem;
|
|
||||||
|
|
||||||
@use "./common.scss";
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: var(--height, $default-height) var(--width, 50px) var(--height, $default-height);
|
|
||||||
grid-template-rows: var(--height, 1fr);
|
|
||||||
border-radius: var(--border-radius, $default-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
span, input {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-family: var(--font-family, inherit);
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-size, inherit);
|
|
||||||
padding: 2px 4px;
|
|
||||||
|
|
||||||
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
margin: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.increment, .decrement {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.increment {
|
|
||||||
border-radius: 0 var(--border-radius, $default-border-radius) var(--border-radius, 4px) 0;
|
|
||||||
}
|
|
||||||
.decrement {
|
|
||||||
border-radius: var(--border-radius, $default-border-radius) 0 0 var(--border-radius, $default-border-radius);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Styling version 1
|
|
||||||
|
|
||||||
@use "./Dialog.scss";
|
|
||||||
|
|
||||||
@use "./player/root.scss";
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
.actor--player.style-v1 {
|
|
||||||
--header-size: 75px;
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--color-underline-header);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
--size: var(--header-size);
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid var(--color-underline-header);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-name {
|
|
||||||
height: var(--header-size);
|
|
||||||
padding: 8px 1rem;
|
|
||||||
font-size: clamp(1rem, 2rem, calc(var(--header-size) - 16px));
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
prose-mirror {
|
|
||||||
--menu-background: rgba(0, 0, 0, 0.1);
|
|
||||||
flex-grow: 1;
|
|
||||||
border: 1px solid var(--color-underline-header);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
system.json
25
system.json
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"id": "taf",
|
"id": "taf",
|
||||||
"title": "Text-Based Actors",
|
"title": "Text-Based Actors",
|
||||||
"description": "",
|
"description": "An intentionally minimalist system that enables you to play rules-light games without any hassle!",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/dotdungeon.zip",
|
"download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/release.zip",
|
||||||
"manifest": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/system.json",
|
"manifest": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/system.json",
|
||||||
"url": "https://github.com/Oliver-Akins/Text-Actors-Foundry",
|
"url": "https://github.com/Oliver-Akins/Text-Actors-Foundry",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": 12,
|
"minimum": 13,
|
||||||
"verified": 12,
|
"verified": 13,
|
||||||
"maximum": 12
|
"maximum": 13
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|
@ -18,12 +18,21 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"src/main.mjs"
|
"./module/main.mjs"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
".styles/root.css"
|
{
|
||||||
|
"src": "./styles/main.css",
|
||||||
|
"layer": "system"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"name": "English (Canadian)",
|
||||||
|
"path": "langs/en-ca.json"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"packs": [],
|
|
||||||
"documentTypes": {
|
"documentTypes": {
|
||||||
"Actor": {
|
"Actor": {
|
||||||
"player": {
|
"player": {
|
||||||
|
|
|
||||||
1
taf.lock
Normal file
1
taf.lock
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
🔒
|
||||||
35
templates/AttributeManager/attribute-list.hbs
Normal file
35
templates/AttributeManager/attribute-list.hbs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="attributes">
|
||||||
|
{{#each attrs as |attr|}}
|
||||||
|
<div
|
||||||
|
class="attribute"
|
||||||
|
data-attribute="{{ attr.id }}"
|
||||||
|
>
|
||||||
|
{{#if attr.isNew}}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-bind="{{ attr.id }}.name"
|
||||||
|
value="{{ attr.name }}"
|
||||||
|
placeholder="Attribute Name..."
|
||||||
|
>
|
||||||
|
{{else}}
|
||||||
|
<span>{{ attr.name }}</span>
|
||||||
|
{{/if}}
|
||||||
|
<label>
|
||||||
|
Has Maximum?
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-bind="{{ attr.id }}.isRange"
|
||||||
|
{{ checked attr.isRange }}
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="removeAttribute"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p>No attributes yet</p>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
13
templates/AttributeManager/controls.hbs
Normal file
13
templates/AttributeManager/controls.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="addNew"
|
||||||
|
>
|
||||||
|
Add New Attribute
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save and Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<div class="ask-dialog">
|
|
||||||
<p>
|
|
||||||
{{ question }}
|
|
||||||
</p>
|
|
||||||
{{#each inputs as | i | }}
|
|
||||||
<div class="prompt">
|
|
||||||
<label
|
|
||||||
for="{{i.id}}"
|
|
||||||
class="prompt__label"
|
|
||||||
>
|
|
||||||
{{ i.label }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="{{i.inputType}}"
|
|
||||||
id="{{i.id}}"
|
|
||||||
class="patrons__input"
|
|
||||||
{{i.valueAttribute}}="{{i.defaultValue}}"
|
|
||||||
{{#if i.autofocus}}autofocus{{/if}}
|
|
||||||
>
|
|
||||||
{{#if i.details}}
|
|
||||||
<p class="prompt__details">
|
|
||||||
{{{ i.details }}}
|
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<form autocomplete="off">
|
|
||||||
<div class="header-row">
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
src="{{actor.img}}"
|
|
||||||
data-edit="img"
|
|
||||||
title="{{actor.name}}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
value="{{actor.name}}"
|
|
||||||
class="actor-name"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{{#if editable}}
|
|
||||||
<prose-mirror
|
|
||||||
class="actor-text"
|
|
||||||
name="system.content"
|
|
||||||
value="{{system.content}}"
|
|
||||||
collaborate="true"
|
|
||||||
data-document-uuid="{{actor.uuid}}"
|
|
||||||
>
|
|
||||||
{{{enriched.system.content}}}
|
|
||||||
</prose-mirror>
|
|
||||||
{{else}}
|
|
||||||
{{{enriched.system.content}}}
|
|
||||||
{{/if}}
|
|
||||||
</form>
|
|
||||||
32
templates/PlayerSheet/attributes.hbs
Normal file
32
templates/PlayerSheet/attributes.hbs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{#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="Current value"
|
||||||
|
>
|
||||||
|
{{#if attr.isRange}}
|
||||||
|
<span aria-hidden="true">/</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="attr-range__max"
|
||||||
|
name="{{attr.path}}.max"
|
||||||
|
value="{{attr.max}}"
|
||||||
|
aria-label="Maximum value"
|
||||||
|
>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<template />
|
||||||
|
{{/if}}
|
||||||
15
templates/PlayerSheet/content.hbs
Normal file
15
templates/PlayerSheet/content.hbs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="content">
|
||||||
|
{{#if editable}}
|
||||||
|
<prose-mirror
|
||||||
|
class="actor-text"
|
||||||
|
name="system.content"
|
||||||
|
value="{{system.content}}"
|
||||||
|
collaborate="true"
|
||||||
|
data-document-uuid="{{actor.uuid}}"
|
||||||
|
>
|
||||||
|
{{{ enriched.system.content }}}
|
||||||
|
</prose-mirror>
|
||||||
|
{{else}}
|
||||||
|
{{{ enriched.system.content }}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
17
templates/PlayerSheet/header.hbs
Normal file
17
templates/PlayerSheet/header.hbs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<header class="sheet-header">
|
||||||
|
<img
|
||||||
|
src="{{actor.img}}"
|
||||||
|
data-action="editImage"
|
||||||
|
data-edit="img"
|
||||||
|
title="{{actor.name}}"
|
||||||
|
height="64"
|
||||||
|
width="64"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
class="large"
|
||||||
|
value="{{actor.name}}"
|
||||||
|
placeholder="{{ localize 'Name' }}"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue