This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View 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";

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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});
}
}

View 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;
}
}

View File

@@ -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"}
]
}
}

View 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);
}
}

View 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"});
}
}