import { CORE_SHAPE, DEFAULT_OVERLAY_CONFIG, OVERLAY_SHAPES } from '../scripts/models.js'; import { VALID_EXPRESSION, getAllEffectMappings } from '../scripts/hooks/effectMappingHooks.js'; import { evaluateOverlayExpressions, genTexture } from '../scripts/token/overlay.js'; import { SEARCH_TYPE } from '../scripts/utils.js'; import { showArtSelect } from '../token-variants.mjs'; import { sortMappingsToGroups } from './effectMappingForm.js'; import { getFlagMappings } from '../scripts/settings.js'; import { Reticle } from '../scripts/reticle.js'; export class OverlayConfig extends FormApplication { constructor(config, callback, token, { mapping, global = false, actor } = {}) { super({}, {}); this.config = config ?? {}; if ((this.config.img || this.config.imgLinked) && !(this.config.img instanceof Array)) { this.config.img = [{ src: this.config.img, linked: this.config.imgLinked }]; } this.config.id = mapping.id; this.callback = callback; this.token = canvas.tokens.get(token._id); this.previewConfig = deepClone(this.config); this.mapping = mapping; this.global = global; this.actor = actor; } static get defaultOptions() { return mergeObject(super.defaultOptions, { id: 'token-variants-overlay-config', classes: ['sheet'], template: 'modules/token-variants/templates/overlayConfig.html', resizable: false, minimizable: false, title: 'Overlay Settings', width: 500, height: 'auto', //tabs: [{ navSelector: '.sheet-tabs', contentSelector: '.content', initial: 'misc' }, ], tabs: [ { navSelector: '.tabs[data-group="main"]', contentSelector: 'form', initial: 'misc' }, { navSelector: '.tabs[data-group="html"]', contentSelector: '.tab[data-tab="html"]', initial: 'template' }, ], }); } get title() { let scope = 'GLOBAL'; if (!this.global && this.actor) { scope = this.actor.name; } return `${this.mapping.label} — [${scope}]`; } /** * @param {JQuery} html */ activateListeners(html) { super.activateListeners(html); html.find('.reticle').on('click', (event) => { const icons = this.getPreviewIcons(); if (icons.length) { Reticle.activate({ tvaOverlay: icons[0].icon, app: this, config: this.previewConfig }); } }); html.find('.repeat').on('change', (event) => { const fieldset = $(event.target).closest('fieldset'); const content = fieldset.find('.content'); if (event.target.checked) { content.show(); fieldset.addClass('active'); } else { content.hide(); fieldset.removeClass('active'); } this.setPosition(); }); // Insert Controls to the Shape Legend const shapeLegends = html.find('.shape-legend'); let config = this.config; shapeLegends.each(function (i) { const legend = $(this); legend.append( ` ` ); if (i != 0) { legend.append( ` ` ); } if (i != shapeLegends.length - 1) { legend.append( ` ` ); } legend.append( `` ); }); // Shape listeners html.find('.addShape').on('click', this._onAddShape.bind(this)); html.find('.addImage').on('click', this._onAddImage.bind(this)); html.find('.addEvent').on('click', this._onAddEvent.bind(this)); html.find('.deleteShape').on('click', this._onDeleteShape.bind(this)); html.find('.deleteImage').on('click', this._onDeleteImage.bind(this)); html.find('.deleteEvent').on('click', this._onDeleteEvent.bind(this)); html.find('.moveShapeUp').on('click', this._onMoveShapeUp.bind(this)); html.find('.moveShapeDown').on('click', this._onMoveShapeDown.bind(this)); html.find('.cloneShape').on('click', this._onCloneShape.bind(this)); html.find('input,select').on('change', this._onInputChange.bind(this)); html.find('textarea').on('change', this._onInputChange.bind(this)); const parentId = html.find('[name="parentID"]'); parentId.on('change', (event) => { if (event.target.value === 'TOKEN') { html.find('.token-specific-fields').show(); } else { html.find('.token-specific-fields').hide(); } this.setPosition(); }); parentId.trigger('change'); html .find('[name="ui"]') .on('change', (event) => { if (parentId.val() === 'TOKEN') { if (event.target.checked) { html.find('.ui-hide').hide(); } else { html.find('.ui-hide').show(); } this.setPosition(); } }) .trigger('change'); html.find('[name="filter"]').on('change', (event) => { html.find('.filterOptions').empty(); const filterOptions = $(genFilterOptionControls(event.target.value)); html.find('.filterOptions').append(filterOptions); this.setPosition({ height: 'auto' }); this.activateListeners(filterOptions); }); html.find('.token-variants-image-select-button').click((event) => { showArtSelect(this.token?.name ?? 'overlay', { searchType: SEARCH_TYPE.TOKEN, callback: (imgSrc, imgName) => { if (imgSrc) $(event.target).closest('.form-group').find('input').val(imgSrc).trigger('change'); }, }); }); html.find('.presetImport').on('click', (event) => { const presetName = $(event.target).closest('.form-group').find('.tmfxPreset').val(); if (presetName) { const preset = TokenMagic.getPreset(presetName); if (preset) { $(event.target) .closest('.form-group') .find('textarea') .val(JSON.stringify(preset, null, 2)) .trigger('change'); } } }); // Controls for locking scale sliders together let scaleState = { locked: true }; // Range inputs need to be triggered when slider moves to initiate preview html .find('.range-value') .siblings('[type="range"]') .on('change', (event) => { $(event.target).siblings('.range-value').val(event.target.value).trigger('change'); }); const lockButtons = $(html).find('.scaleLock > a'); const sliderScaleWidth = $(html).find('[name="scaleX"]'); const sliderScaleHeight = $(html).find('[name="scaleY"]'); const sliderWidth = html.find('.scaleX'); const sliderHeight = html.find('.scaleY'); lockButtons.on('click', function () { scaleState.locked = !scaleState.locked; lockButtons.html(scaleState.locked ? '' : ''); }); sliderScaleWidth.on('change', function () { if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) { sliderScaleHeight.val(sliderScaleWidth.val()).trigger('change'); sliderHeight.val(sliderScaleWidth.val()); } }); sliderScaleHeight.on('change', function () { if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) { sliderScaleWidth.val(sliderScaleHeight.val()).trigger('change'); sliderWidth.val(sliderScaleHeight.val()); } }); html.on('change', '.scaleX', () => { sliderScaleWidth.trigger('change'); }); html.on('change', '.scaleY', () => { sliderScaleHeight.trigger('change'); }); html.find('.me-edit-json').on('click', async (event) => { const textarea = $(event.target).closest('.form-group').find('textarea'); let params; try { params = eval(textarea.val()); } catch (e) { console.warn('TVA |', e); } if (params) { let param; if (Array.isArray(params)) { if (params.length === 1) param = params[0]; else { let i = await promptParamChoice(params); if (i < 0) return; param = params[i]; } } else { param = params; } if (param) game.modules.get('multi-token-edit').api.showGenericForm(param, param.filterType ?? 'TMFX', { inputChangeCallback: (selected) => { mergeObject(param, selected, { inplace: true }); textarea.val(JSON.stringify(params, null, 2)).trigger('change'); }, }); } }); const underlay = html.find('[name="underlay"]'); const top = html.find('[name="top"]'); const bottom = html.find('[name="bottom"]'); underlay.change(function () { if (this.checked) top.prop('checked', false); else bottom.prop('checked', false); }); top.change(function () { if (this.checked) { underlay.prop('checked', false); bottom.prop('checked', false); } }); bottom.change(function () { if (this.checked) { underlay.prop('checked', true); top.prop('checked', false); } }); const linkScale = html.find('[name="linkScale"]'); const linkDimensions = html.find('[name="linkDimensionsX"], [name="linkDimensionsY"]'); const linkStageScale = html.find('[name="linkStageScale"]'); linkScale.change(function () { if (this.checked) { linkDimensions.prop('checked', false); linkStageScale.prop('checked', false); } }); linkDimensions.change(function () { if (this.checked) { linkScale.prop('checked', false); linkStageScale.prop('checked', false); } }); linkStageScale.change(function () { if (this.checked) { linkScale.prop('checked', false); linkDimensions.prop('checked', false); } }); // Setting border color for property expression const limitOnProperty = html.find('[name="limitOnProperty"]'); limitOnProperty.on('input', (event) => { const input = $(event.target); if (input.val() === '') { input.removeClass('tvaValid'); input.removeClass('tvaInvalid'); } else if (input.val().match(VALID_EXPRESSION)) { input.addClass('tvaValid'); input.removeClass('tvaInvalid'); } else { input.addClass('tvaInvalid'); input.removeClass('tvaValid'); } }); limitOnProperty.trigger('input'); html.find('.create-variable').on('click', this._onCreateVariable.bind(this)); html.find('.delete-variable').on('click', this._onDeleteVariable.bind(this)); } _onDeleteVariable(event) { let index = $(event.target).closest('tr').data('index'); if (index != null) { this.config = this._getSubmitData(); if (!this.config.variables) this.config.variables = []; this.config.variables.splice(index, 1); this.render(true); } } _onCreateVariable(event) { this.config = this._getSubmitData(); if (!this.config.variables) this.config.variables = []; this.config.variables.push({ name: '', value: '' }); this.render(true); } _onAddShape(event) { let shape = $(event.target).siblings('select').val(); shape = deepClone(OVERLAY_SHAPES[shape]); shape = mergeObject(deepClone(CORE_SHAPE), { shape }); this.config = this._getSubmitData(); if (!this.config.shapes) this.config.shapes = []; this.config.shapes.push(shape); this.render(true); } _onAddImage(event) { this.config = this._getSubmitData(); if (!this.config.img) this.config.img = []; else if (!(this.config.img instanceof Array)) { this.config.img = [{ src: this.config.img, linked: this.config.imgLinked }]; } this.config.img.push({ src: '', linked: false }); this.render(true); } _onAddEvent(event) { let listener = $(event.target).siblings('select').val(); this.config = this._getSubmitData(); if (!this.config.interactivity) this.config.interactivity = []; this.config.interactivity.push({ listener, macro: '', script: '' }); this.render(true); } _onDeleteShape(event) { const index = $(event.target).closest('.deleteShape').data('index'); if (index == null) return; this.config = this._getSubmitData(); if (!this.config.shapes) this.config.shapes = []; this.config.shapes.splice(index, 1); this.render(true); } _onDeleteImage(event) { const index = $(event.target).closest('.deleteImage').data('index'); if (index == null) return; this.config = this._getSubmitData(); if (!(this.config.img instanceof Array)) this.config.img = []; else this.config.img.splice(index, 1); if (!this.config.img.length) this.config.img = ''; this.render(true); } _onDeleteEvent(event) { const index = $(event.target).closest('.deleteEvent').data('index'); if (index == null) return; this.config = this._getSubmitData(); if (!this.config.interactivity) this.config.interactivity = []; this.config.interactivity.splice(index, 1); this.render(true); } _onCloneShape(event) { const index = $(event.target).closest('.cloneShape').data('index'); if (!index && index != 0) return; this.config = this._getSubmitData(); if (!this.config.shapes) return; const nShape = deepClone(this.config.shapes[index]); if (nShape.label) { nShape.label = nShape.label + ' - Copy'; } this.config.shapes.push(nShape); this.render(true); } _onMoveShapeUp(event) { const index = $(event.target).closest('.moveShapeUp').data('index'); if (!index) return; this.config = this._getSubmitData(); if (!this.config.shapes) this.config.shapes = []; if (this.config.shapes.length >= 2) this._swapShapes(index, index - 1); this.render(true); } _onMoveShapeDown(event) { const index = $(event.target).closest('.moveShapeDown').data('index'); if (!index && index != 0) return; this.config = this._getSubmitData(); if (!this.config.shapes) this.config.shapes = []; if (this.config.shapes.length >= 2) this._swapShapes(index, index + 1); this.render(true); } _swapShapes(i1, i2) { let temp = this.config.shapes[i1]; this.config.shapes[i1] = this.config.shapes[i2]; this.config.shapes[i2] = temp; } _convertColor(colString) { try { const c = Color.fromString(colString); const rgba = c.rgb; rgba.push(1); return rgba; } catch (e) { return [1, 1, 1, 1]; } } async _onInputChange(event) { this.previewConfig = this._getSubmitData(); if (event.target.type === 'color') { const color = $(event.target).siblings('.color'); color.val(event.target.value).trigger('change'); return; } this._applyPreviews(); } getPreviewIcons() { if (!this.config.id) return []; const tokens = this.token ? [this.token] : canvas.tokens.placeables; const previewIcons = []; for (const tkn of tokens) { if (tkn.tvaOverlays) { for (const c of tkn.tvaOverlays) { if (c.overlayConfig && c.overlayConfig.id === this.config.id) { // Effect icon found, however if we're in global preview then we need to take into account // a token/actor specific mapping which may override the global one if (this.token) { previewIcons.push({ token: tkn, icon: c }); } else if (!getFlagMappings(tkn).find((m) => m.id === this.config.id)) { previewIcons.push({ token: tkn, icon: c }); } } } } } return previewIcons; } async _applyPreviews() { const targets = this.getPreviewIcons(); for (const target of targets) { const preview = evaluateOverlayExpressions(deepClone(this.previewConfig), target.token, { overlayConfig: this.previewConfig, }); target.icon.refresh(preview, { preview: true, previewTexture: await genTexture(target.token, preview), refreshFilters: true, }); } } async _removePreviews() { const targets = this.getPreviewIcons(); for (const target of targets) { target.icon.refresh(null, { refreshFilters: true }); } } async getData(options) { const data = super.getData(options); data.filters = Object.keys(PIXI.filters); data.filters.push('OutlineOverlayFilter'); data.filters.sort(); data.tmfxActive = game.modules.get('tokenmagic')?.active; if (data.tmfxActive) { data.tmfxPresets = TokenMagic.getPresets().map((p) => p.name); data.filters.unshift('Token Magic FX'); } data.filters.unshift('NONE'); const settings = mergeObject(DEFAULT_OVERLAY_CONFIG, this.config, { inplace: false, }); data.ceActive = game.modules.get('dfreds-convenient-effects')?.active; if (data.ceActive) { data.ceEffects = game.dfreds.effects.all.map((ef) => ef.name); } data.macros = game.macros.map((m) => m.name); if (settings.filter !== 'NONE') { const filterOptions = genFilterOptionControls(settings.filter, settings.filterOptions); if (filterOptions) { settings.filterOptions = filterOptions; } else { settings.filterOptions = null; } } else { settings.filterOptions = null; } data.users = game.users.map((u) => { return { id: u.id, name: u.name, selected: settings.limitedUsers.includes(u.id) }; }); data.fonts = Object.keys(CONFIG.fontDefinitions); const allMappings = getAllEffectMappings(this.token, true).filter((m) => m.id !== this.config.id); const [_, groupedMappings] = sortMappingsToGroups(allMappings); data.parents = groupedMappings; if (!data.parentID) data.parentID = 'TOKEN'; if (!data.anchor) data.anchor = { x: 0.5, y: 0.5 }; // Cache Partials for (const shapeName of Object.keys(OVERLAY_SHAPES)) { await getTemplate(`modules/token-variants/templates/partials/shape${shapeName}.html`); } await getTemplate('modules/token-variants/templates/partials/repeating.html'); await getTemplate('modules/token-variants/templates/partials/interpolateColor.html'); data.allShapes = Object.keys(OVERLAY_SHAPES); data.textAlignmentOptions = [ { value: 'left', label: 'Left' }, { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' }, { value: 'justify', label: 'Justify' }, ]; // linkDimensions has been converted to linkDimensionsX and linkDimensionsY // Make sure we're using the latest fields // 20/07/2023 if (!('linkDimensionsX' in settings) && settings.linkDimensions) { settings.linkDimensionsX = true; settings.linkDimensionsY = true; } return mergeObject(data, settings); } _getHeaderButtons() { const buttons = super._getHeaderButtons(); buttons.unshift({ label: 'Core Variables', class: '.core-variables', icon: 'fas fa-file-import fa-fw', onclick: () => { let content = `
Variable | Description |
---|---|
@hp | Actor Health |
@hpMax | Actor Health (Max) |
@gridSize | Grid Size (Pixels) |
@label | Mapping's Label Field |