diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b32a7..6a80774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,18 +19,22 @@ Added: * You cannot add a duplicate modifier (there are some exceptions) * Support of Sequencer 2.1.9 * A bunch of missed arguments for modifiers + * `fadeDuration` for toggle/show/hide sections + * `addSequence` section Fixed: * Wrong width of argument labels * A couple of rare problems in the hooks' calling * Sequencer's warning about reading db before it's populated * Many problems with arguments' inputs (i suppose, i also added some more bugs) - * Fixed missing dependencies for v9 + * Missed dependencies for v9 * Manual position picking reset controlled tokens to only the first one. Known bugs: * Color picker for stroke color in text modifier is bugged * Alpha in color pickers do nothing + * Toggle/show/hide sections ignores `fadeDuration` in export as code + * Non-sequencer sections (hide, kill, so on) with `multiply` cannot be played separately ## [0.7.2](https://github.com/averrin/director/compare/0.7.1...0.7.2) diff --git a/README.md b/README.md index 5083ca7..b93c6b0 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Your actions are stored in the scene's data so that you can have many of them. T Director sometimes looks too complex, so read [Documentation](https://github.com/averrin/director/wiki/How-to-use.-With-examples.). It contains a couple of easy to go examples. ## Hints +- [JB2A](https://foundryvtt.com/packages/JB2A_DnD5e/) is not required but HIGHLY recommended - you can drag and drop tags - right click on tag opens a color picker -- right click on action item opens a color picker for its accent - if director fails to open, please try `Director.clearSceneData()` or `Director.clearGlobalData()` to clear data. - click on "visible/hidden" tag in a selection card toggles visibility - You can [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/averrin) @@ -34,6 +34,7 @@ It's not going to be MATT's alternative, so actions are pretty basic - Stop Sequencer's effects - Manage Convenient Effects - Manage TokenMagicFX filters +- Manage FxMaster weather effects ## Integrations * Tagger [required] @@ -41,17 +42,20 @@ It's not going to be MATT's alternative, so actions are pretty basic * Monk's Active Tiles Triggers * Token Magic FX [optional Virtual Sequencer's section] * DFreds Convenient Effects [optional Virtual Sequencer's section] +* FxMaster ## Plans - [ ] Documentation - [X] Extension API +- [ ] Migrate to Active Effects Manager - [ ] Token Attacher integration - [ ] Warpgate mutations & summons integration +- [ ] Support for templates, lights, doors and so on - [ ] Rolls support (variables, hooks) - [X] v10 support - [ ] Beginner / Expert UI mode - [ ] Better import / export -- [ ] Better support of Sequencer's functions and types +- [X] Better support of Sequencer's functions and types - [X] Better support for Token Magic FX - [X] Support for Convenient Effects - [ ] Hooks for ending effects of a sequence. E.g. to destroy DAE's effects. diff --git a/src/modules/Actions.js b/src/modules/Actions.js index 82d9786..8d05edd 100644 --- a/src/modules/Actions.js +++ b/src/modules/Actions.js @@ -41,10 +41,12 @@ export default class Action { const spec = actionTypes.find(t => t.id == this.type.id); const value = override || this.value; let objects; - objects = await calculateValue(value, "selection"); + if (!spec.ignoreTarget || value === "") { + objects = await calculateValue(value, "selection"); + } if (!Array.isArray(objects)) objects = [objects]; if (spec?.execute) { - if (spec.ignoreTarget) { + if (spec.ignoreTarget || value === "") { spec.execute(null, this, event, seqVars); } else { objects.forEach((o) => spec.execute(o, this, event, seqVars)); diff --git a/src/modules/Integrations.js b/src/modules/Integrations.js index 42251fc..17306be 100644 --- a/src/modules/Integrations.js +++ b/src/modules/Integrations.js @@ -20,7 +20,7 @@ function initActiveTilesIntegration() { label: 'Execute trigger', group: 'Active Tiles', execute: (object, action, event) => { - if (object.data.flags["monks-active-tiles"]) { + if (object && object.data.flags["monks-active-tiles"]) { globalThis.game.MonksActiveTiles.object = { document: object }; globalThis.game.MonksActiveTiles.manuallyTrigger(event); } @@ -78,6 +78,7 @@ function initTokenMagicIntegration() { label: 'Toggle Token Magic', group: 'Token Magic', execute: (object, action, event) => { + if (!object) return; object.getFlag = object.document.getFlag.bind(object.document); if (TokenMagic.hasFilterType(object, action.args[0])) { TokenMagic.deleteFilters(object, action.args[0]); @@ -94,6 +95,7 @@ function initTokenMagicIntegration() { label: 'Add Token Magic', group: 'Token Magic', execute: (object, action, event) => { + if (!object) return; const filter = TokenMagic.getPreset(action.args[0]); TokenMagic.addUpdateFilters(object, filter); }, args: [{ type: 'token-magic', label: 'filter' }] @@ -105,6 +107,7 @@ function initTokenMagicIntegration() { label: 'Remove Token Magic', group: 'Token Magic', execute: (object, action, event) => { + if (!object) return; TokenMagic.deleteFilters(object, action.args[0]); }, args: [{ type: 'token-magic', label: 'filter' }] }); @@ -125,7 +128,7 @@ function initFxMasterIntegration() { label: 'Set Weather', group: 'FxMaster', ignoreTarget: true, - execute: (object, action, event) => { + execute: (_, action, event) => { Hooks.call("fxmaster.switchWeather", { type: action.args[0], id: "weather", options: {} }); }, args: [{ type: 'weather', label: 'weather' }] }); @@ -136,7 +139,7 @@ function initFxMasterIntegration() { label: 'Clear Weather', group: 'FxMaster', ignoreTarget: true, - execute: (object, action, event) => { + execute: (_, action, event) => { canvas.scene.unsetFlag("fxmaster", "effects"); }, args: [] }); @@ -218,7 +221,7 @@ function initConvenientEffectsIntegration() { label: 'Toggle Convenient Effect', group: 'Convenient Effects', execute: (object, action, event) => { - game.dfreds.effectInterface.toggleEffect(action.args[0], { uuids: [object.actor.uuid] }); + object && game.dfreds.effectInterface.toggleEffect(action.args[0], { uuids: [object.actor.uuid] }); }, args: [{ type: 'ce-effect', label: 'effect' }] }); @@ -228,7 +231,7 @@ function initConvenientEffectsIntegration() { label: 'Add Convenient Effect', group: 'Convenient Effects', execute: (object, action, event) => { - game.dfreds.effectInterface.addEffect({ effectName: action.args[0], uuid: object.actor.uuid }); + object && game.dfreds.effectInterface.addEffect({ effectName: action.args[0], uuid: object.actor.uuid }); }, args: [{ type: 'ce-effect', label: 'effect' }] }); @@ -239,7 +242,7 @@ function initConvenientEffectsIntegration() { label: 'Remove Convenient Effect', group: 'Convenient Effects', execute: (object, action, event) => { - game.dfreds.effectInterface.removeEffect({ effectName: action.args[0], uuid: object.actor.uuid }); + object && game.dfreds.effectInterface.removeEffect({ effectName: action.args[0], uuid: object.actor.uuid }); }, args: [{ type: 'ce-effect', label: 'effect' }] }); diff --git a/src/modules/Sequencer.js b/src/modules/Sequencer.js index 52a40af..d1d2170 100644 --- a/src/modules/Sequencer.js +++ b/src/modules/Sequencer.js @@ -121,7 +121,7 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; let r = `// This macro was created by converting Director's sequence. Correctness of this code isn't guaranteed.\n`; r += `// The code isn't indentical to how Director executes sequences, but it should be a good base for modifications.\n`; if (this.variables.length > 0) { - r += `\n//==== Variables (all of them, not only used) ====\n`; + r += `\n//==== Variables (all of them, not only used) ====//\n`; } for (const v of this.variables) { const [val, replace] = this.getCodeForVal(v.name, v.value, v.type); @@ -132,7 +132,7 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; } } - r += `\n//==== Sequence construction ====\n`; + r += `\n//==== Sequence construction ====//\n`; r += `let sequence = new Sequence("${moduleId}")`; let i = 0; @@ -144,16 +144,20 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; for (const m of section.modifiers) { if (m.type == "multiply") { isMulti = true; - mode = m.args[1] || "on"; + mode = m.args[1] || section._spec.multiplyMode; targets = m.args[0]; } } if (isMulti) { section.modifiers = section.modifiers.filter(m => m.type != "multiply"); - const mod = new Modifier(uuidv4(), mode); - mod.setType(mode, section.type); - mod.args[0] = "@_target"; - section.modifiers.push(mod); + if (mode) { + const mod = new Modifier(uuidv4(), mode); + mod.setType(mode, section.type); + mod.args[0] = "@_target"; + section.modifiers.push(mod); + } else { + section.args[0] = "@_target"; + } if (stage == "normal") r += ";"; r += `\n\nfor (const _target of ${this.getCodeForVal("", targets, "selection")[0]}) {`; stage = "in_multi"; @@ -178,6 +182,8 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; r += `.${section.type}()`; r += `\n\t\t.origin("${this.id}")`; r += `\n\t\t.name(${name})`; + } else if (section.type == "addSequence") { + r += `/* .${section.type}() */ // please combine sequences by yourself`; } else { if (section._spec && "toCode" in section._spec) { r += section._spec.toCode(args); @@ -261,6 +267,11 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; val = await v.getValue(this); } } + if (!obj._spec.args) { + n++; + result.push(undefined); + continue; + } const spec = obj._spec.args[n]; if (val === undefined) { val = await calculateValue(a, spec.type, this); @@ -292,7 +303,7 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; if (m.type == "multiply") { isMulti = true; targets = await calculateValue(m.args[0], "selection"); - mode = m.args[1] || "on"; + mode = m.args[1] || section._spec.multiplyMode; } } if (!isMulti) { @@ -302,10 +313,14 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; const ns = Section.fromPlain(section); ns.id = uuidv4(); ns.modifiers = ns.modifiers.filter(m => m.type != "multiply"); - const mod = new Modifier(uuidv4(), mode); - mod.setType(mode, section.type); - mod.args[0] = target; - ns.modifiers.push(mod); + if (mode == "self") { + ns.args[0] = target; + } else { + const mod = new Modifier(uuidv4(), mode); + mod.setType(mode, section.type); + mod.args[0] = target; + ns.modifiers.push(mod); + } seq.push(ns); } } @@ -319,6 +334,9 @@ controlled.forEach(c => c.control({releaseOthers: false}));`; if ("thenDo" in section._spec) { sectionName = "thenDo"; args = [section._spec.thenDo(args)]; + } else if ("addSequence" in section._spec) { + sectionName = "addSequence"; + args = [section._spec.addSequence(args)]; } let currentSection = s[sectionName](...args); if (section.type == "effect") { @@ -424,6 +442,7 @@ export class Section { this.args = []; this.type = type; this._spec = sectionSpecs.find(s => s.id == this.type); + this.modifiers = []; if (!this._spec || !this._spec.args) return; for (const arg of this._spec?.args) { const spec = argSpecs.find(s => s.id == arg.type); diff --git a/src/modules/Specs.js b/src/modules/Specs.js index bdee57c..9b3e714 100644 --- a/src/modules/Specs.js +++ b/src/modules/Specs.js @@ -7,31 +7,31 @@ export const actionTypes = [ id: 'toggle', label: 'Toggle visibility', group: 'Common', - execute: (object, action) => tools.toggle(object?.document || object), + execute: (object, action) => object && tools.toggle(object?.document || object), }, { id: 'hide', label: 'Hide', group: 'Common', - execute: (object, action) => tools.hide(object?.document || object), + execute: (object, action) => object && tools.hide(object?.document || object), }, { id: 'show', label: 'Show', group: 'Common', - execute: (object, action) => tools.show(object?.document || object), + execute: (object, action) => object && tools.show(object?.document || object), }, { id: 'kill', label: 'Kill', group: 'Tokens', - execute: (object, action) => tools.kill(object?.document || object), + execute: (object, action) => object && tools.kill(object?.document || object), }, { id: 'revive', label: 'Revive', group: 'Tokens', - execute: (object, action) => tools.revive(object?.document || object), + execute: (object, action) => object && tools.revive(object?.document || object), }, { id: 'playSequence', @@ -40,7 +40,9 @@ export const actionTypes = [ execute: (object, action, event, seqVars) => { const overrides = seqVars || {}; - overrides[action.args[1]?.name || action.args[1]] = object; + if (object) { + overrides[action.args[1]?.name || action.args[1]] = object; + } globalThis.Director.playSequence(action.args[0], overrides); }, args: [{ type: "sequence", label: "sequence" }, { type: "sequence-vars", label: "var" }] @@ -73,71 +75,117 @@ export function addAction(action) { export const sectionSpecs = [ { id: 'effect', label: 'Effect', args: [{ type: 'string', label: 'name' }], group: "Sequencer", collapsible: true }, - { id: 'animation', label: 'Animation', group: "Sequencer", collapsible: true }, + { + id: 'animation', label: 'Animation', group: "Sequencer", collapsible: true, + multiplyMode: "on", + }, { id: 'sound', label: 'Sound', group: "Sequencer", collapsible: true }, { id: 'wait', label: 'Wait', args: [{ type: 'int', label: 'ms' }], group: "Sequencer", nonPlayable: true }, { id: 'macro', label: 'Macro', args: [{ type: 'macro', label: 'name' }], group: "Sequencer" }, { id: 'thenDo', label: 'thenDo', args: [{ type: 'code', label: 'func' }], group: "Sequencer" }, + { id: 'addSequence', label: 'addSequence', args: [{ type: 'sequence', label: 'sequence' }], group: "Sequencer" }, { id: 'toggle', label: 'Toggle visibility', - args: [{ type: 'placeable', label: 'target' }], + args: [{ type: 'placeable', label: 'target' }, { type: "int", label: "fadeDuration" }], group: "Actions", - thenDo: (args) => () => tools.toggle(args[0].document), - toCode: (args) => `\t.thenDo(async () => ${args[0]}.document.update({ hidden: !${args[0]}.document.data.hidden }))\n`, + multiplyMode: "self", + addSequence: (args) => { + let s = new globalThis.Sequence("director"); + if (args[1] != 0) { + if (args[0].data.hidden || args[0].document?.data?.hidden) { + s = s.animation().on(args[0]).opacity(0) + .thenDo(() => tools.show(args[0].document || args[0])) + .animation().on(args[0]).fadeIn(args[1]); + } else { + s = s.animation().on(args[0]).fadeOut(args[1]).wait(args[1]) + .thenDo(() => tools.hide(args[0].document || args[0])).wait(15) + .animation().on(args[0]).opacity(1); + } + } else { + s.thenDo(() => tools.toggle(args[0].document || args[0])); + } + return s; + }, + toCode: (args) => `.thenDo(async () => ${args[0]}.document.update({ hidden: !${args[0]}.document.data.hidden }))`, }, { id: 'show', label: 'Show', - args: [{ type: 'placeable', label: 'target' }], + args: [{ type: 'placeable', label: 'target' }, { type: "int", label: "fadeDuration" }], group: "Actions", - thenDo: (args) => () => tools.show(args[0].document), - toCode: (args) => `\t.thenDo(async () => ${args[0]}.document.update({ hidden: false }))\n`, + multiplyMode: "self", + addSequence: (args) => { + let s = new globalThis.Sequence("director"); + if (args[1] > 0) { + s = s.animation().on(args[0]).opacity(0) + .thenDo(() => tools.show(args[0].document || args[0])) + .animation().on(args[0]).fadeIn(args[1]); + } else { + s.thenDo(() => tools.show(args[0].document || args[0])); + } + return s; + }, + toCode: (args) => `.thenDo(async () => ${args[0]}.document.update({ hidden: false }))`, }, { id: 'hide', label: 'Hide', - args: [{ type: 'placeable', label: 'target' }], + args: [{ type: 'placeable', label: 'target' }, { type: "int", label: "fadeDuration" }], group: "Actions", - thenDo: (args) => () => tools.hide(args[0].document), - toCode: (args) => `\t.thenDo(async () => ${args[0]}.document.update({ hidden: true }))\n`, + multiplyMode: "self", + addSequence: (args) => { + let s = new globalThis.Sequence("director"); + if (args[1] > 0) { + s = s.animation().on(args[0]).fadeOut(args[1]).wait(args[1]) + .thenDo(() => tools.hide(args[0].document || args[0])).wait(500) + .animation().on(args[0]).opacity(1); + } else { + s.thenDo(() => tools.hide(args[0].document || args[0])); + } + return s; + }, + toCode: (args) => `.thenDo(async () => ${args[0]}.document.update({ hidden: true }))`, }, { id: 'kill', label: 'Kill', args: [{ type: 'token', label: 'target' }], group: "Actions", + multiplyMode: "self", thenDo: (args) => () => tools.kill(args[0].document), - toCode: (args) => `\t.thenDo(async () => ${args[0]}.document.actor.update({ + toCode: (args) => `.thenDo(async () => ${args[0]}.document.actor.update({ "data.attributes.hp.value": 0, - }))\n`, + }))`, }, { id: 'revive', label: 'Revive', args: [{ type: 'token', label: 'target' }], group: "Actions", + multiplyMode: "self", thenDo: (args) => () => tools.revive(args[0].document), - toCode: (args) => `\t.thenDo(async () => ${args[0]}.document.actor.update({ + toCode: (args) => `.thenDo(async () => ${args[0]}.document.actor.update({ "data.attributes.hp.value": ${args[0]}.document.actor.getRollData().attributes.hp.max, - }))\n`, + }))`, }, { id: 'endEffect', label: 'End Effect', args: [{ type: 'effectSource', label: 'effect' }], group: "Sequencer", + multiplyMode: "self", toCode: (_args) => { const args = [..._args].flat(); if (args[0] == "#sceneId") { - return `\t.thenDo(async () => Sequencer.EffectManager.endEffects())\n`; + return `.thenDo(async () => Sequencer.EffectManager.endEffects())`; } else if (args[0] == "#origin" && args.length > 1) { - return `\t.thenDo(async () => Sequencer.EffectManager.endEffects({ origin: ${args[1].id} }))\n`; + return `.thenDo(async () => Sequencer.EffectManager.endEffects({ origin: ${args[1].id} }))`; } else if (args.length >= 2) { const f = {}; f[args[0].slice(1)] = args[1]; - return `\t.thenDo(async () => Sequencer.EffectManager.endEffects(${JSON.stringify(f)}))\n`; + return `.thenDo(async () => Sequencer.EffectManager.endEffects(${JSON.stringify(f)}))`; } return ``; }, @@ -163,6 +211,13 @@ export function addSection(section) { } export const modifierSpecs = [ + + { id: 'multiply', group: 'toggle', args: [{ type: 'targets', label: 'targets' }], cat: "Special" }, + { id: 'multiply', group: 'show', args: [{ type: 'targets', label: 'targets' }], cat: "Special" }, + { id: 'multiply', group: 'hide', args: [{ type: 'targets', label: 'targets' }], cat: "Special" }, + { id: 'multiply', group: 'kill', args: [{ type: 'targets', label: 'targets' }], cat: "Special" }, + { id: 'multiply', group: 'revive', args: [{ type: 'targets', label: 'targets' }], cat: "Special" }, + //Effect { id: 'file', group: 'effect', args: [{ type: 'effect_file', label: 'file' }], cat: "Required" }, { id: 'atLocation', group: 'effect', args: [{ type: 'position', label: 'pos' }], cat: "Required" }, @@ -534,7 +589,7 @@ export const hookSpecs = [ name: "% of Actor's property change", parents: ["updateActor"], target: targetFromActor, - test: (target, prop, ts, prop2, actor, _, updates) => { + test: (target, prop, ts, abs, prop2, actor, _, updates) => { let d = getProperty(updates.prevData, prop) - getProperty(actor.getRollData(), prop); if (abs) d = Math.abs(d); return d / getProperty(updates.prevData, prop2) * 100 >= ts; @@ -619,6 +674,7 @@ export const argSpecs = [ }, { id: "targets", options: [ + { value: "", label: "No target", group: "General" }, { value: "#controlled.all", label: "All Controlled", group: "Controlled" }, { value: "#controlled.random", label: "Random Controlled", group: "Controlled" }, { value: "#controlled.first", label: "First Controlled", group: "Controlled" }, diff --git a/src/modules/helpers.js b/src/modules/helpers.js index 866d7ad..5fabc67 100644 --- a/src/modules/helpers.js +++ b/src/modules/helpers.js @@ -144,6 +144,8 @@ export function calculateValueSync(val, type, seq) { let code = `'use strict'; try {${val}} catch(e) {}`; const f = new AsyncFunction(...Object.keys(vars), code) return () => f(...Object.values(vars)); + } else if (type == "sequence") { + return Director.getSequence(val); } } else if (Array.isArray(val)) { if (type == "effectSource" || type == "hookData") { diff --git a/src/styles/main.scss b/src/styles/main.scss index 5b56739..c61f5fb 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -8,7 +8,7 @@ } #director.window-app { - min-width: 25%; + min-width: 60rem; max-width: 80%; max-height: 90%; --tw-bg-opacity: 0.9 !important; diff --git a/src/view/components/SequenceEditor.svelte b/src/view/components/SequenceEditor.svelte index 089f2ea..2de7d66 100644 --- a/src/view/components/SequenceEditor.svelte +++ b/src/view/components/SequenceEditor.svelte @@ -146,6 +146,12 @@ } let specs = sectionSpecs; + + function doShowAddMod(item) { + let m = modifierSpecs.filter((m) => item.type == m.group); + m = m.filter((m) => m.multi || !item.modifiers.find((mod) => mod.type == m.id)); + return m.length > 0; + } {#if seq} @@ -247,7 +253,7 @@ vars={seq.variables} /> {/each} - {#if modifierSpecs.filter((m) => item.type == m.group).length > 0} + {#if doShowAddMod(item)}