Initial
This commit is contained in:
7
resources/app/client-esm/applications/sheets/_module.mjs
Normal file
7
resources/app/client-esm/applications/sheets/_module.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
export {default as ActorSheetV2} from "./actor-sheet.mjs";
|
||||
export {default as AmbientSoundConfig} from "./ambient-sound-config.mjs";
|
||||
export {default as AmbientLightConfig} from "./ambient-light-config.mjs";
|
||||
export {default as ItemSheetV2} from "./item-sheet.mjs";
|
||||
export {default as RegionBehaviorConfig} from "./region-behavior-config.mjs";
|
||||
export {default as RegionConfig} from "./region-config.mjs";
|
||||
export {default as UserConfig} from "./user-config.mjs";
|
||||
130
resources/app/client-esm/applications/sheets/actor-sheet.mjs
Normal file
130
resources/app/client-esm/applications/sheets/actor-sheet.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
|
||||
/**
|
||||
* A base class for providing Actor Sheet behavior using ApplicationV2.
|
||||
*/
|
||||
export default class ActorSheetV2 extends DocumentSheetV2 {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
position: {
|
||||
width: 600
|
||||
},
|
||||
window: {
|
||||
controls: [
|
||||
{
|
||||
action: "configurePrototypeToken",
|
||||
icon: "fa-solid fa-user-circle",
|
||||
label: "TOKEN.TitlePrototype",
|
||||
ownership: "OWNER"
|
||||
},
|
||||
{
|
||||
action: "showPortraitArtwork",
|
||||
icon: "fa-solid fa-image",
|
||||
label: "SIDEBAR.CharArt",
|
||||
ownership: "OWNER"
|
||||
},
|
||||
{
|
||||
action: "showTokenArtwork",
|
||||
icon: "fa-solid fa-image",
|
||||
label: "SIDEBAR.TokenArt",
|
||||
ownership: "OWNER"
|
||||
}
|
||||
]
|
||||
},
|
||||
actions: {
|
||||
configurePrototypeToken: ActorSheetV2.#onConfigurePrototypeToken,
|
||||
showPortraitArtwork: ActorSheetV2.#onShowPortraitArtwork,
|
||||
showTokenArtwork: ActorSheetV2.#onShowTokenArtwork,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The Actor document managed by this sheet.
|
||||
* @type {ClientDocument}
|
||||
*/
|
||||
get actor() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* If this sheet manages the ActorDelta of an unlinked Token, reference that Token document.
|
||||
* @type {TokenDocument|null}
|
||||
*/
|
||||
get token() {
|
||||
return this.document.token || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getHeaderControls() {
|
||||
const controls = this.options.window.controls;
|
||||
|
||||
// Portrait image
|
||||
const img = this.actor.img;
|
||||
if ( img === CONST.DEFAULT_TOKEN ) controls.findSplice(c => c.action === "showPortraitArtwork");
|
||||
|
||||
// Token image
|
||||
const pt = this.actor.prototypeToken;
|
||||
const tex = pt.texture.src;
|
||||
if ( pt.randomImg || [null, undefined, CONST.DEFAULT_TOKEN].includes(tex) ) {
|
||||
controls.findSplice(c => c.action === "showTokenArtwork");
|
||||
}
|
||||
return controls;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _renderHTML(context, options) {
|
||||
return `<p>TESTING</p>`;
|
||||
}
|
||||
|
||||
_replaceHTML(result, content, options) {
|
||||
content.insertAdjacentHTML("beforeend", result);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle header control button clicks to render the Prototype Token configuration sheet.
|
||||
* @this {ActorSheetV2}
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onConfigurePrototypeToken(event) {
|
||||
event.preventDefault();
|
||||
const renderOptions = {
|
||||
left: Math.max(this.position.left - 560 - 10, 10),
|
||||
top: this.position.top
|
||||
};
|
||||
new CONFIG.Token.prototypeSheetClass(this.actor.prototypeToken, renderOptions).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle header control button clicks to display actor portrait artwork.
|
||||
* @this {ActorSheetV2}
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onShowPortraitArtwork(event) {
|
||||
const {img, name, uuid} = this.actor;
|
||||
new ImagePopout(img, {title: name, uuid: uuid}).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle header control button clicks to display actor portrait artwork.
|
||||
* @this {ActorSheetV2}
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onShowTokenArtwork(event) {
|
||||
const {prototypeToken, name, uuid} = this.actor;
|
||||
new ImagePopout(prototypeToken.texture.src, {title: name, uuid: uuid}).render(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
|
||||
/**
|
||||
* The AmbientLight configuration application.
|
||||
* @extends DocumentSheetV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias AmbientLightConfig
|
||||
*/
|
||||
export default class AmbientLightConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["ambient-light-config"],
|
||||
window: {
|
||||
contentClasses: ["standard-form"]
|
||||
},
|
||||
position: {
|
||||
width: 560,
|
||||
height: "auto"
|
||||
},
|
||||
form: {
|
||||
handler: this.#onSubmit,
|
||||
closeOnSubmit: true
|
||||
},
|
||||
actions:{
|
||||
reset: this.#onReset
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
tabs: {
|
||||
template: "templates/generic/tab-navigation.hbs"
|
||||
},
|
||||
basic: {
|
||||
template: "templates/scene/parts/light-basic.hbs"
|
||||
},
|
||||
animation: {
|
||||
template: "templates/scene/parts/light-animation.hbs"
|
||||
},
|
||||
advanced: {
|
||||
template: "templates/scene/parts/light-advanced.hbs"
|
||||
},
|
||||
footer: {
|
||||
template: "templates/generic/form-footer.hbs"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain a copy of the original to show a real-time preview of changes.
|
||||
* @type {AmbientLightDocument}
|
||||
*/
|
||||
preview;
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
sheet: "basic"
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Application Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preRender(context, options) {
|
||||
await super._preRender(context, options);
|
||||
if ( this.preview?.rendered ) {
|
||||
await this.preview.object.draw();
|
||||
this.document.object.initializeLightSource({deleted: true});
|
||||
this.preview.object.layer.preview.addChild(this.preview.object);
|
||||
this._previewChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
this.#toggleReset();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onClose(options) {
|
||||
super._onClose(options);
|
||||
if ( this.preview ) this._resetPreview();
|
||||
if ( this.document.rendered ) this.document.object.initializeLightSource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(options) {
|
||||
|
||||
// Create the preview on first render
|
||||
if ( options.isFirstRender && this.document.object ) {
|
||||
const clone = this.document.object.clone();
|
||||
this.preview = clone.document;
|
||||
}
|
||||
|
||||
// Prepare context
|
||||
const document = this.preview ?? this.document;
|
||||
const isDarkness = document.config.negative;
|
||||
return {
|
||||
light: document,
|
||||
source: document.toObject(),
|
||||
fields: document.schema.fields,
|
||||
colorationTechniques: AdaptiveLightingShader.SHADER_TECHNIQUES,
|
||||
gridUnits: document.parent.grid.units || game.i18n.localize("GridUnits"),
|
||||
isDarkness,
|
||||
lightAnimations: isDarkness ? CONFIG.Canvas.darknessAnimations : CONFIG.Canvas.lightAnimations,
|
||||
tabs: this.#getTabs(),
|
||||
buttons: [
|
||||
{
|
||||
type: "reset",
|
||||
action: "reset",
|
||||
icon: "fa-solid fa-undo",
|
||||
label: "AMBIENT_LIGHT.ACTIONS.RESET"
|
||||
},
|
||||
{
|
||||
type: "submit",
|
||||
icon: "fa-solid fa-save",
|
||||
label: this.document.id ? "AMBIENT_LIGHT.ACTIONS.UPDATE" : "AMBIENT_LIGHT.ACTIONS.CREATE"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
basic: {id: "basic", group: "sheet", icon: "fa-solid fa-lightbulb", label: "AMBIENT_LIGHT.SECTIONS.BASIC"},
|
||||
animation: {id: "animation", group: "sheet", icon: "fa-solid fa-play", label: "AMBIENT_LIGHT.SECTIONS.ANIMATION"},
|
||||
advanced: {id: "advanced", group: "sheet", icon: "fa-solid fa-cogs", label: "AMBIENT_LIGHT.SECTIONS.ADVANCED"}
|
||||
}
|
||||
for ( const v of Object.values(tabs) ) {
|
||||
v.active = this.tabGroups[v.group] === v.id;
|
||||
v.cssClass = v.active ? "active" : "";
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Toggle visibility of the reset button which is only visible on the advanced tab.
|
||||
*/
|
||||
#toggleReset() {
|
||||
const reset = this.element.querySelector("button[data-action=reset]");
|
||||
reset.classList.toggle("hidden", this.tabGroups.sheet !== "advanced");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
changeTab(...args) {
|
||||
super.changeTab(...args);
|
||||
this.#toggleReset();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Real-Time Preview */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onChangeForm(formConfig, event) {
|
||||
super._onChangeForm(formConfig, event);
|
||||
const formData = new FormDataExtended(this.element);
|
||||
this._previewChanges(formData.object);
|
||||
|
||||
// Special handling for darkness state change
|
||||
if ( event.target.name === "config.negative") this.render({parts: ["animation", "advanced"]});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Preview changes to the AmbientLight document as if they were true document updates.
|
||||
* @param {object} [change] A change to preview.
|
||||
* @protected
|
||||
*/
|
||||
_previewChanges(change) {
|
||||
if ( !this.preview ) return;
|
||||
if ( change ) this.preview.updateSource(change);
|
||||
if ( this.preview?.rendered ) {
|
||||
this.preview.object.renderFlags.set({refresh: true});
|
||||
this.preview.object.initializeLightSource();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Restore the true data for the AmbientLight document when the form is submitted or closed.
|
||||
* @protected
|
||||
*/
|
||||
_resetPreview() {
|
||||
if ( !this.preview ) return;
|
||||
if ( this.preview.rendered ) {
|
||||
this.preview.object.destroy({children: true});
|
||||
}
|
||||
this.preview = null;
|
||||
if ( this.document.rendered ) {
|
||||
const object = this.document.object;
|
||||
object.renderable = true;
|
||||
object.initializeLightSource();
|
||||
object.renderFlags.set({refresh: true});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Process form submission for the sheet.
|
||||
* @param {SubmitEvent} event The originating form submission event
|
||||
* @param {HTMLFormElement} form The form element that was submitted
|
||||
* @param {FormDataExtended} formData Processed data for the submitted form
|
||||
* @this {AmbientLightConfig}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async #onSubmit(event, form, formData) {
|
||||
const submitData = this._prepareSubmitData(event, form, formData);
|
||||
if ( this.document.id ) await this.document.update(submitData);
|
||||
else await this.document.constructor.create(submitData, {parent: canvas.scene});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Process reset button click
|
||||
* @param {PointerEvent} event The originating button click
|
||||
* @this {AmbientLightConfig}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async #onReset(event) {
|
||||
event.preventDefault();
|
||||
const defaults = AmbientLightDocument.cleanData();
|
||||
const keys = ["vision", "config"];
|
||||
const configKeys = ["coloration", "contrast", "attenuation", "luminosity", "saturation", "shadows"];
|
||||
for ( const k in defaults ) {
|
||||
if ( !keys.includes(k) ) delete defaults[k];
|
||||
}
|
||||
for ( const k in defaults.config ) {
|
||||
if ( !configKeys.includes(k) ) delete defaults.config[k];
|
||||
}
|
||||
this._previewChanges(defaults);
|
||||
await this.render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
|
||||
/**
|
||||
* The AmbientSound configuration application.
|
||||
* @extends DocumentSheetV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias AmbientSoundConfig
|
||||
*/
|
||||
export default class AmbientSoundConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["ambient-sound-config"],
|
||||
window: {
|
||||
contentClasses: ["standard-form"]
|
||||
},
|
||||
position: {
|
||||
width: 560,
|
||||
height: "auto"
|
||||
},
|
||||
form: {
|
||||
handler: this.#onSubmit,
|
||||
closeOnSubmit: true
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
body: {
|
||||
template: "templates/scene/ambient-sound-config.hbs"
|
||||
},
|
||||
footer: {
|
||||
template: "templates/generic/form-footer.hbs"
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
get title() {
|
||||
if ( !this.document.id ) return game.i18n.localize("AMBIENT_SOUND.ACTIONS.CREATE");
|
||||
return super.title;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(_options) {
|
||||
return {
|
||||
sound: this.document,
|
||||
source: this.document.toObject(),
|
||||
fields: this.document.schema.fields,
|
||||
gridUnits: this.document.parent.grid.units || game.i18n.localize("GridUnits"),
|
||||
soundEffects: CONFIG.soundEffects,
|
||||
buttons: [{
|
||||
type: "submit",
|
||||
icon: "fa-solid fa-save",
|
||||
label: game.i18n.localize(this.document.id ? "AMBIENT_SOUND.ACTIONS.UPDATE" : "AMBIENT_SOUND.ACTIONS.CREATE")
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onRender(context, options) {
|
||||
this.#toggleDisabledFields();
|
||||
return super._onRender(context, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onClose(_options) {
|
||||
if ( !this.document.id ) canvas.sounds.clearPreviewContainer();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onChangeForm(formConfig, event) {
|
||||
this.#toggleDisabledFields();
|
||||
return super._onChangeForm(formConfig, event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special logic to toggle the disabled state of form fields depending on the values of other fields.
|
||||
*/
|
||||
#toggleDisabledFields() {
|
||||
const form = this.element;
|
||||
form["effects.base.intensity"].disabled = !form["effects.base.type"].value;
|
||||
form["effects.muffled.type"].disabled = form.walls.checked;
|
||||
form["effects.muffled.intensity"].disabled = form.walls.checked || !form["effects.muffled.type"].value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Process form submission for the sheet.
|
||||
* @param {SubmitEvent} event The originating form submission event
|
||||
* @param {HTMLFormElement} form The form element that was submitted
|
||||
* @param {FormDataExtended} formData Processed data for the submitted form
|
||||
* @this {AmbientSoundConfig}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async #onSubmit(event, form, formData) {
|
||||
const submitData = this._prepareSubmitData(event, form, formData);
|
||||
if ( this.document.id ) await this.document.update(submitData);
|
||||
else await this.document.constructor.create(submitData, {parent: canvas.scene});
|
||||
}
|
||||
}
|
||||
30
resources/app/client-esm/applications/sheets/item-sheet.mjs
Normal file
30
resources/app/client-esm/applications/sheets/item-sheet.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
|
||||
/**
|
||||
* A base class for providing Item Sheet behavior using ApplicationV2.
|
||||
*/
|
||||
export default class ItemSheetV2 extends DocumentSheetV2 {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
position: {
|
||||
width: 480
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The Item document managed by this sheet.
|
||||
* @type {ClientDocument}
|
||||
*/
|
||||
get item() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Actor instance which owns this Item, if any.
|
||||
* @type {Actor|null}
|
||||
*/
|
||||
get actor() {
|
||||
return this.document.actor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").FormNode} FormNode
|
||||
* @typedef {import("../_types.mjs").FormFooterButton} FormFooterButton
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Scene Region configuration application.
|
||||
* @extends DocumentSheetV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias RegionBehaviorConfig
|
||||
*/
|
||||
export default class RegionBehaviorConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.options.window.icon = CONFIG.RegionBehavior.typeIcons[this.document.type];
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["region-behavior-config"],
|
||||
window: {
|
||||
contentClasses: ["standard-form"],
|
||||
icon: undefined // Defined in constructor
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: "auto"
|
||||
},
|
||||
form: {
|
||||
closeOnSubmit: true
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
form: {
|
||||
template: "templates/generic/form-fields.hbs"
|
||||
},
|
||||
footer: {
|
||||
template: "templates/generic/form-footer.hbs"
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Context Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(_options) {
|
||||
const doc = this.document;
|
||||
return {
|
||||
region: doc,
|
||||
source: doc._source,
|
||||
fields: this._getFields(),
|
||||
buttons: this._getButtons()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare form field structure for rendering.
|
||||
* @returns {FormNode[]}
|
||||
*/
|
||||
_getFields() {
|
||||
const doc = this.document;
|
||||
const source = doc._source;
|
||||
const fields = doc.schema.fields;
|
||||
const {events, ...systemFields} = CONFIG.RegionBehavior.dataModels[doc.type]?.schema.fields;
|
||||
const fieldsets = [];
|
||||
|
||||
// Identity
|
||||
fieldsets.push({
|
||||
fieldset: true,
|
||||
legend: "BEHAVIOR.SECTIONS.identity",
|
||||
fields: [
|
||||
{field: fields.name, value: source.name}
|
||||
]
|
||||
});
|
||||
|
||||
// Status
|
||||
fieldsets.push({
|
||||
fieldset: true,
|
||||
legend: "BEHAVIOR.SECTIONS.status",
|
||||
fields: [
|
||||
{field: fields.disabled, value: source.disabled}
|
||||
]
|
||||
});
|
||||
|
||||
// Subscribed events
|
||||
if ( events ) {
|
||||
fieldsets.push({
|
||||
fieldset: true,
|
||||
legend: "BEHAVIOR.TYPES.base.SECTIONS.events",
|
||||
fields: [
|
||||
{field: events, value: source.system.events}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Other system fields
|
||||
const sf = {fieldset: true, legend: CONFIG.RegionBehavior.typeLabels[doc.type], fields: []};
|
||||
this.#addSystemFields(sf, systemFields, source);
|
||||
if ( sf.fields.length ) fieldsets.push(sf);
|
||||
return fieldsets;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Recursively add system model fields to the fieldset.
|
||||
*/
|
||||
#addSystemFields(fieldset, schema, source, _path="system") {
|
||||
for ( const field of Object.values(schema) ) {
|
||||
const path = `${_path}.${field.name}`;
|
||||
if ( field instanceof foundry.data.fields.SchemaField ) {
|
||||
this.#addSystemFields(fieldset, field.fields, source, path);
|
||||
}
|
||||
else if ( field.constructor.hasFormSupport ) {
|
||||
fieldset.fields.push({field, value: foundry.utils.getProperty(source, path)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get footer buttons for this behavior config sheet.
|
||||
* @returns {FormFooterButton[]}
|
||||
* @protected
|
||||
*/
|
||||
_getButtons() {
|
||||
return [
|
||||
{type: "submit", icon: "fa-solid fa-save", label: "BEHAVIOR.ACTIONS.update"}
|
||||
]
|
||||
}
|
||||
}
|
||||
383
resources/app/client-esm/applications/sheets/region-config.mjs
Normal file
383
resources/app/client-esm/applications/sheets/region-config.mjs
Normal file
@@ -0,0 +1,383 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
import {DOCUMENT_OWNERSHIP_LEVELS} from "../../../common/constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").ApplicationTab} ApplicationTab
|
||||
* @typedef {import("../_types.mjs").FormFooterButton} FormFooterButton
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Scene Region configuration application.
|
||||
* @extends DocumentSheetV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias RegionConfig
|
||||
*/
|
||||
export default class RegionConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["region-config"],
|
||||
window: {
|
||||
contentClasses: ["standard-form"],
|
||||
icon: "fa-regular fa-game-board"
|
||||
},
|
||||
position: {
|
||||
width: 480,
|
||||
height: "auto"
|
||||
},
|
||||
form: {
|
||||
closeOnSubmit: true
|
||||
},
|
||||
viewPermission: DOCUMENT_OWNERSHIP_LEVELS.OWNER,
|
||||
actions: {
|
||||
shapeCreateFromWalls: RegionConfig.#onShapeCreateFromWalls,
|
||||
shapeToggleHole: RegionConfig.#onShapeToggleHole,
|
||||
shapeMoveUp: RegionConfig.#onShapeMoveUp,
|
||||
shapeMoveDown: RegionConfig.#onShapeMoveDown,
|
||||
shapeRemove: RegionConfig.#onShapeRemove,
|
||||
behaviorCreate: RegionConfig.#onBehaviorCreate,
|
||||
behaviorDelete: RegionConfig.#onBehaviorDelete,
|
||||
behaviorEdit: RegionConfig.#onBehaviorEdit,
|
||||
behaviorToggle: RegionConfig.#onBehaviorToggle
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
tabs: {
|
||||
template: "templates/generic/tab-navigation.hbs"
|
||||
},
|
||||
identity: {
|
||||
template: "templates/scene/parts/region-identity.hbs"
|
||||
},
|
||||
shapes: {
|
||||
template: "templates/scene/parts/region-shapes.hbs",
|
||||
scrollable: [".scrollable"]
|
||||
},
|
||||
behaviors: {
|
||||
template: "templates/scene/parts/region-behaviors.hbs",
|
||||
scrollable: [".scrollable"]
|
||||
},
|
||||
footer: {
|
||||
template: "templates/generic/form-footer.hbs"
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
sheet: "identity"
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Context Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(_options) {
|
||||
const doc = this.document;
|
||||
return {
|
||||
region: doc,
|
||||
source: doc.toObject(),
|
||||
fields: doc.schema.fields,
|
||||
tabs: this.#getTabs(),
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _preparePartContext(partId, context) {
|
||||
const doc = this.document;
|
||||
switch ( partId ) {
|
||||
case "footer":
|
||||
context.buttons = this.#getFooterButtons();
|
||||
break;
|
||||
case "behaviors":
|
||||
context.tab = context.tabs.behaviors;
|
||||
context.behaviors = doc.behaviors.map(b => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
typeLabel: game.i18n.localize(CONFIG.RegionBehavior.typeLabels[b.type]),
|
||||
typeIcon: CONFIG.RegionBehavior.typeIcons[b.type] || "fa-regular fa-notdef",
|
||||
disabled: b.disabled
|
||||
})).sort((a, b) => (a.disabled - b.disabled) || a.name.localeCompare(b.name, game.i18n.lang));
|
||||
break;
|
||||
case "identity":
|
||||
context.tab = context.tabs.identity;
|
||||
break;
|
||||
case "shapes":
|
||||
context.tab = context.tabs.shapes;
|
||||
break;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
this.element.querySelectorAll(".region-shape").forEach(e => {
|
||||
e.addEventListener("mouseover", this.#onShapeHoverIn.bind(this));
|
||||
e.addEventListener("mouseout", this.#onShapeHoverOut.bind(this));
|
||||
});
|
||||
this.document.object?.renderFlags.set({refreshState: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onClose(options) {
|
||||
super._onClose(options);
|
||||
this.document.object?.renderFlags.set({refreshState: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
identity: {id: "identity", group: "sheet", icon: "fa-solid fa-tag", label: "REGION.SECTIONS.identity"},
|
||||
shapes: {id: "shapes", group: "sheet", icon: "fa-solid fa-shapes", label: "REGION.SECTIONS.shapes"},
|
||||
behaviors: {id: "behaviors", group: "sheet", icon: "fa-solid fa-child-reaching", label: "REGION.SECTIONS.behaviors"}
|
||||
}
|
||||
for ( const v of Object.values(tabs) ) {
|
||||
v.active = this.tabGroups[v.group] === v.id;
|
||||
v.cssClass = v.active ? "active" : "";
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an array of form footer buttons.
|
||||
* @returns {Partial<FormFooterButton>[]}
|
||||
*/
|
||||
#getFooterButtons() {
|
||||
return [
|
||||
{type: "submit", icon: "fa-solid fa-save", label: "REGION.ACTIONS.update"}
|
||||
]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse-hover events on a shape.
|
||||
*/
|
||||
#onShapeHoverIn(event) {
|
||||
event.preventDefault();
|
||||
if ( !this.document.parent.isView ) return;
|
||||
const index = this.#getControlShapeIndex(event);
|
||||
canvas.regions._highlightShape(this.document.shapes[index]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse-unhover events for shape.
|
||||
*/
|
||||
#onShapeHoverOut(event) {
|
||||
event.preventDefault();
|
||||
if ( !this.document.parent.isView ) return;
|
||||
canvas.regions._highlightShape(null);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to move the shape up.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onShapeMoveUp(event) {
|
||||
if ( this.document.shapes.length <= 1 ) return;
|
||||
const index = this.#getControlShapeIndex(event);
|
||||
if ( index === 0 ) return;
|
||||
const shapes = [...this.document.shapes];
|
||||
[shapes[index - 1], shapes[index]] = [shapes[index], shapes[index - 1]];
|
||||
await this.document.update({shapes});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to move the shape down.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onShapeMoveDown(event) {
|
||||
if ( this.document.shapes.length <= 1 ) return;
|
||||
const index = this.#getControlShapeIndex(event);
|
||||
if ( index === this.document.shapes.length - 1 ) return;
|
||||
const shapes = [...this.document.shapes];
|
||||
[shapes[index], shapes[index + 1]] = [shapes[index + 1], shapes[index]];
|
||||
await this.document.update({shapes});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to create shapes from the controlled walls.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onShapeCreateFromWalls(event) {
|
||||
event.preventDefault(); // Don't open context menu
|
||||
event.stopPropagation(); // Don't trigger other events
|
||||
if ( !canvas.ready || (event.detail > 1) ) return; // Ignore repeated clicks
|
||||
|
||||
// If no walls are controlled, inform the user they need to control walls
|
||||
if ( !canvas.walls.controlled.length ) {
|
||||
if ( canvas.walls.active ) {
|
||||
ui.notifications.error("REGION.NOTIFICATIONS.NoControlledWalls", {localize: true});
|
||||
}
|
||||
else {
|
||||
canvas.walls.activate({tool: "select"});
|
||||
ui.notifications.info("REGION.NOTIFICATIONS.ControlWalls", {localize: true});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the shape
|
||||
const polygons = canvas.walls.identifyInteriorArea(canvas.walls.controlled);
|
||||
if ( polygons.length === 0 ) {
|
||||
ui.notifications.error("REGION.NOTIFICATIONS.EmptyEnclosedArea", {localize: true});
|
||||
return;
|
||||
}
|
||||
const shapes = polygons.map(p => new foundry.data.PolygonShapeData({points: p.points}));
|
||||
|
||||
// Merge the new shape with form submission data
|
||||
const form = this.element;
|
||||
const formData = new FormDataExtended(form);
|
||||
const submitData = this._prepareSubmitData(event, form, formData);
|
||||
submitData.shapes = [...this.document._source.shapes, ...shapes];
|
||||
|
||||
// Update the region
|
||||
await this.document.update(submitData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to toggle the hold field of a shape.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onShapeToggleHole(event) {
|
||||
const index = this.#getControlShapeIndex(event);
|
||||
const shapes = this.document.shapes.map(s => s.toObject());
|
||||
shapes[index].hole = !shapes[index].hole;
|
||||
await this.document.update({shapes});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to remove a shape.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onShapeRemove(event) {
|
||||
const index = this.#getControlShapeIndex(event);
|
||||
let shapes = this.document.shapes;
|
||||
return foundry.applications.api.DialogV2.confirm({
|
||||
window: {
|
||||
title: game.i18n.localize("REGION.ACTIONS.shapeRemove")
|
||||
},
|
||||
content: `<p>${game.i18n.localize("AreYouSure")}</p>`,
|
||||
rejectClose: false,
|
||||
yes: {
|
||||
callback: () => {
|
||||
// Test that there haven't been any changes to the shapes since the dialog the button was clicked
|
||||
if ( this.document.shapes !== shapes ) return false;
|
||||
shapes = [...shapes];
|
||||
shapes.splice(index, 1);
|
||||
this.document.update({shapes});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the shape index from a control button click.
|
||||
* @param {PointerEvent} event The button-click event
|
||||
* @returns {number} The shape index
|
||||
*/
|
||||
#getControlShapeIndex(event) {
|
||||
const button = event.target;
|
||||
const li = button.closest(".region-shape");
|
||||
return Number(li.dataset.shapeIndex);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle button clicks to create a new behavior.
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onBehaviorCreate(_event) {
|
||||
await RegionBehavior.implementation.createDialog({}, {parent: this.document});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to delete a behavior.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onBehaviorDelete(event) {
|
||||
const behavior = this.#getControlBehavior(event);
|
||||
await behavior.deleteDialog();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to edit a behavior.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onBehaviorEdit(event) {
|
||||
const target = event.target;
|
||||
if ( target.closest(".region-element-name") && (event.detail !== 2) ) return; // Double-click on name
|
||||
const behavior = this.#getControlBehavior(event);
|
||||
await behavior.sheet.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to toggle a behavior.
|
||||
* @param {PointerEvent} event
|
||||
* @this {RegionConfig}
|
||||
*/
|
||||
static async #onBehaviorToggle(event) {
|
||||
const behavior = this.#getControlBehavior(event);
|
||||
await behavior.update({disabled: !behavior.disabled});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the RegionBehavior document from a control button click.
|
||||
* @param {PointerEvent} event The button-click event
|
||||
* @returns {RegionBehavior} The region behavior document
|
||||
*/
|
||||
#getControlBehavior(event) {
|
||||
const button = event.target;
|
||||
const li = button.closest(".region-behavior");
|
||||
return this.document.behaviors.get(li.dataset.behaviorId);
|
||||
}
|
||||
}
|
||||
118
resources/app/client-esm/applications/sheets/user-config.mjs
Normal file
118
resources/app/client-esm/applications/sheets/user-config.mjs
Normal file
@@ -0,0 +1,118 @@
|
||||
import DocumentSheetV2 from "../api/document-sheet.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
|
||||
/**
|
||||
* The User configuration application.
|
||||
* @extends DocumentSheetV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias UserConfig
|
||||
*/
|
||||
export default class UserConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["user-config"],
|
||||
position: {
|
||||
width: 480,
|
||||
height: "auto"
|
||||
},
|
||||
actions: {
|
||||
releaseCharacter: UserConfig.#onReleaseCharacter
|
||||
},
|
||||
form: {
|
||||
closeOnSubmit: true
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
form: {
|
||||
id: "form",
|
||||
template: "templates/sheets/user-config.hbs"
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
get title() {
|
||||
return `${game.i18n.localize("PLAYERS.ConfigTitle")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(_options) {
|
||||
return {
|
||||
user: this.document,
|
||||
source: this.document.toObject(),
|
||||
fields: this.document.schema.fields,
|
||||
characterWidget: this.#characterChoiceWidget.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render the Character field as a choice between observed Actors.
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
#characterChoiceWidget(field, _groupConfig, inputConfig) {
|
||||
|
||||
// Create the form field
|
||||
const fg = document.createElement("div");
|
||||
fg.className = "form-group stacked character";
|
||||
const ff = fg.appendChild(document.createElement("div"));
|
||||
ff.className = "form-fields";
|
||||
fg.insertAdjacentHTML("beforeend", `<p class="hint">${field.hint}</p>`);
|
||||
|
||||
// Actor select
|
||||
const others = game.users.reduce((s, u) => {
|
||||
if ( u.character && !u.isSelf ) s.add(u.character.id);
|
||||
return s;
|
||||
}, new Set());
|
||||
|
||||
const options = [];
|
||||
const ownerGroup = game.i18n.localize("OWNERSHIP.OWNER");
|
||||
const observerGroup = game.i18n.localize("OWNERSHIP.OBSERVER");
|
||||
for ( const actor of game.actors ) {
|
||||
if ( !actor.testUserPermission(this.document, "OBSERVER") ) continue;
|
||||
const a = {value: actor.id, label: actor.name, disabled: others.has(actor.id)};
|
||||
options.push({group: actor.isOwner ? ownerGroup : observerGroup, ...a});
|
||||
}
|
||||
|
||||
const input = foundry.applications.fields.createSelectInput({...inputConfig,
|
||||
name: field.fieldPath,
|
||||
options,
|
||||
blank: "",
|
||||
sort: true
|
||||
});
|
||||
ff.appendChild(input);
|
||||
|
||||
// Player character
|
||||
const c = this.document.character;
|
||||
if ( c ) {
|
||||
ff.insertAdjacentHTML("afterbegin", `<img class="avatar" src="${c.img}" alt="${c.name}">`);
|
||||
const release = `<button type="button" class="icon fa-solid fa-ban" data-action="releaseCharacter"
|
||||
data-tooltip="USER.SHEET.BUTTONS.RELEASE"></button>`
|
||||
ff.insertAdjacentHTML("beforeend", release);
|
||||
}
|
||||
return fg;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to release the currently selected character.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onReleaseCharacter(event) {
|
||||
event.preventDefault();
|
||||
const button = event.target;
|
||||
const fields = button.parentElement;
|
||||
fields.querySelector("select[name=character]").value = "";
|
||||
fields.querySelector("img.avatar").remove();
|
||||
button.remove();
|
||||
this.setPosition({height: "auto"});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user