diff --git a/assets/icons/consequence-icons/csq-complication-severe.svg b/assets/icons/consequence-icons/csq-complication-serious.svg similarity index 100% rename from assets/icons/consequence-icons/csq-complication-severe.svg rename to assets/icons/consequence-icons/csq-complication-serious.svg diff --git a/assets/icons/consequence-icons/gm-csq-button.svg b/assets/icons/consequence-icons/gm-csq-button.svg new file mode 100644 index 00000000..a8cf0df5 --- /dev/null +++ b/assets/icons/consequence-icons/gm-csq-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/consequence-icons/player-resist-button.svg b/assets/icons/consequence-icons/player-resist-button.svg new file mode 100644 index 00000000..dd5952bb --- /dev/null +++ b/assets/icons/consequence-icons/player-resist-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/style.min.css b/css/style.min.css index 001ef7a3..f465fc2c 100644 --- a/css/style.min.css +++ b/css/style.min.css @@ -15124,6 +15124,7 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog { max-width: 900px; height: auto !important; + width: auto !important; --item-info-height: 100px; --buttons-height: 25px; } @@ -15245,6 +15246,45 @@ template { padding: 0; height: var(--buttons-height); } +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section { + width: 600px; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .button-icon { + position: unset; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .roll-consequence-entry-row .consequence-type-icon { + width: 24px; + min-width: 24px; + filter: brightness(1.5) drop-shadow(0 0 4px black); +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .roll-consequence-entry-row .roll-consequence-type-select { + font-family: Oswald; + flex-basis: 150px; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .roll-consequence-entry-row .roll-consequence-attribute-select { + font-family: Oswald; + flex-basis: 95px; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .roll-consequence-entry-row .consequence-name { + background: rgba(0, 0, 0, 0.5); + flex-basis: 375px; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .consequence-resist-options-container { + width: 90%; + margin-left: 10%; + justify-content: stretch; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .consequence-resist-options-container .consequence-resist-option { + width: 100%; + justify-content: stretch; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .consequence-resist-options-container .consequence-resist-option .toggle-icon { + position: unset; +} +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.dialog .window-content .consequence-section .roll-consequence-row .consequence-resist-options-container .consequence-resist-option .shadowed.consequence-name { + flex-grow: 1; + background: rgba(0, 0, 0, 0.5); +} :root body.vtt.game.system-eunos-blades .app.window-app.sheet.active-effect-sheet { min-width: 600px; height: auto !important; @@ -15273,6 +15313,7 @@ template { flex-grow: 3; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab { + --roll-spacing: 5px; height: auto !important; width: unset !important; overflow: visible; @@ -15393,7 +15434,7 @@ template { width: auto; max-width: 175px; margin: 0; - right: calc(100% + 3px); + right: calc(100% + 2 * var(--roll-spacing, 5px) - 2px); background: rgb(24, 24, 24); top: 0px; padding: 2px 0 2px 5px; @@ -15800,10 +15841,6 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .roll-sheet-float-block.effect-final-block .pos-effect-trade-block .fa-light { scale: 0.8; } -:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form { - min-width: var(--full-roll-width, 550px); - max-width: var(--full-roll-width, 550px); -} :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form.roll-type-action { --full-roll-width: 775px; } @@ -15953,13 +15990,9 @@ template { border: 3px outset var(--blades-white); background: var(--blades-black-dark); } -:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form.roll-type-action { - min-width: 775px; - max-width: 775px; -} -:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form.roll-type-resistance { - min-width: 500px; - max-width: 500px; +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form { + min-width: var(--full-roll-width, 550px); + max-width: var(--full-roll-width, 550px); } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form.roll-type-resistance .sheet-root { border-radius: 30px; @@ -16300,9 +16333,15 @@ template { position: relative; gap: 5px 5px; } +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .roll-sheet-block.roll-participants-block { + border: none; +} :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .roll-sheet-block.rolling-block { min-height: 62px; } +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .roll-sheet-block.rolling-block .roll-sheet-sub-block.roll-header-block { + margin-top: -2px; +} :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .roll-sheet-block.rolling-block .roll-sheet-sub-block.roll-header-block .roll-sheet-select, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .roll-sheet-block.rolling-block .roll-sheet-sub-block.roll-header-block .roll-readonly { background: var(--blades-white-bright); @@ -16330,6 +16369,8 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root { align-items: stretch; position: static; + gap: var(--roll-spacing, 5px); + padding: var(--roll-spacing, 5px); } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .split-root .split-root-left, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .split-root .split-root-right, @@ -16347,8 +16388,8 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-left.split-root-left, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-right.split-root-left, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .full-root-container.split-root-left { - flex-basis: 75%; - max-width: 370px; + flex-basis: 85%; + max-width: 85%; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .split-root .split-root-left.split-root-right, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .split-root .split-root-right.split-root-right, @@ -16356,10 +16397,10 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-left.split-root-right, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-right.split-root-right, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .full-root-container.split-root-right { - flex-basis: 35%; + flex-basis: 15%; display: flex; align-items: stretch; - justify-content: stretch; + justify-content: space-between; flex-direction: column; z-index: 4; } @@ -16369,7 +16410,6 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-left.split-root-right section.sheet-main, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .split-root-right.split-root-right section.sheet-main, :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root .full-root .full-root-container.split-root-right section.sheet-main { - flex-grow: 1; z-index: 0; overflow: hidden; } @@ -16660,14 +16700,17 @@ template { height: 30px; width: 100%; } +:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-sub-block.roll-effects-block.inactive-mod-block { + padding-right: 40px; + margin-right: -40px; + z-index: -2; +} :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-effects-block { z-index: 4; width: 100%; position: absolute; pointer-events: none; margin-top: 20px; -} -:root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-effects-block:not(.inactive-mod-block) { flex-direction: column; width: unset; align-items: flex-start; @@ -16761,6 +16804,7 @@ template { position: absolute; translate: -50% 100%; left: 50%; + top: 50%; font-family: var(--font-emphasis); color: var(--blades-white); text-shadow: 0 0 4px var(--blades-black-dark), 0 0 4px var(--blades-black-dark), 0 0 4px var(--blades-black-dark), 0 0 4px var(--blades-black-dark); diff --git a/module/BladesDialog.js b/module/BladesDialog.js index 0119ec85..bc46d607 100644 --- a/module/BladesDialog.js +++ b/module/BladesDialog.js @@ -7,6 +7,10 @@ import { ApplyTooltipListeners } from "./core/gsap.js"; import U from "./core/utilities.js"; +import BladesActor from "./BladesActor.js"; +import BladesRoll from "./BladesRoll.js"; +import C, { RollResult, AttributeTrait } from "./core/constants.js"; +import BladesAI, { AGENTS } from "./core/ai.js"; export var SelectionCategory; (function (SelectionCategory) { SelectionCategory["Heritage"] = "Heritage"; @@ -36,11 +40,10 @@ export var BladesDialogType; BladesDialogType["Selection"] = "Selection"; BladesDialogType["Consequence"] = "Consequence"; })(BladesDialogType || (BladesDialogType = {})); -class BladesSelectorDialog extends Dialog { +class BladesDialog extends Dialog { static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { classes: ["eunos-blades", "sheet", "dialog"], - template: "systems/eunos-blades/templates/dialog.hbs", width: "auto", height: "auto", tabs: [{ navSelector: ".nav-tabs", contentSelector: ".tab-content", initial: "front" }] @@ -53,7 +56,7 @@ class BladesSelectorDialog extends Dialog { ]); } static async DisplaySelectionDialog(parent, title, docType, tabs, tags) { - const app = new BladesSelectorDialog({ + const app = new BladesDialog({ parent, title, docType, @@ -64,13 +67,42 @@ class BladesSelectorDialog extends Dialog { cancel: { icon: '', label: game.i18n.localize("Cancel"), - callback: () => false + callback: (html) => { + eLog.checkLog3("dialog", "Callback Scope", { this: this, html }); + return false; + } } }, default: "cancel" }); return app.hasItems ? app.render(true, { width: app.width }) : undefined; } + static async DisplayRollConsequenceDialog(rollInst) { + const app = new BladesDialog({ + parent: rollInst, + title: "Consequences", + dialogType: BladesDialogType.Consequence, + content: "", + buttons: { + apply: { + icon: '', + label: "Apply", + callback: (html) => app + .writeToRollInstance(html) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: (html) => { + eLog.checkLog3("dialog", "Callback Scope", { this: app, html }); + return false; + } + } + }, + default: "apply" + }, { classes: ["eunos-blades", "sheet", "dialog", "consequence-dialog"] }); + return app._render(true, { width: app.width }).then(() => eLog.checkLog3("dialog", "Dialog Instance", { this: app })); + } get template() { if (this.dialogType === BladesDialogType.Selection) { return "systems/eunos-blades/templates/dialog-selection.hbs"; @@ -78,7 +110,7 @@ class BladesSelectorDialog extends Dialog { return "systems/eunos-blades/templates/dialog-consequence.hbs"; } get hasItems() { - return Object.values(this.tabs).some((tabItems) => tabItems.length > 0); + return Object.values(this.tabs ?? []).some((tabItems) => tabItems.length > 0); } parent; tabs; @@ -86,9 +118,27 @@ class BladesSelectorDialog extends Dialog { tags = []; width; docType; + csqData = { [RollResult.partial]: {}, [RollResult.fail]: {} }; constructor(data, options) { super(data, options); + this.dialogType = data.dialogType ?? BladesDialogType.Selection; + this.parent = data.parent; + this.width = 500; + switch (this.dialogType) { + case BladesDialogType.Selection: + this.constructSelectionData(data ); + return; + case BladesDialogType.Consequence: + this.constructConsequenceData(data ); + return; + default: throw new Error(`Unrecognized type for BladesDialog constructor: '${this.dialogType}'`); + } + } + constructSelectionData(data ) { const validTabs = []; + if (!data.tabs) { + return; + } for (const [tabName, tabItems] of Object.entries(data.tabs)) { if (tabItems.length === 0) { delete data.tabs[tabName]; @@ -101,32 +151,193 @@ class BladesSelectorDialog extends Dialog { data.tabs.Main = [...data.tabs[validTabs[0]]]; delete data.tabs[validTabs[0]]; } - this.dialogType = data.dialogType ?? BladesDialogType.Selection; this.docType = data.docType; - this.parent = data.parent; this.tabs = data.tabs; this.tags = data.tags ?? []; this.width = 150 * Math.ceil(Math.sqrt(Object.values(data.tabs)[0].length)); } + constructConsequenceData(data ) { + eLog.checkLog3("dialog", "constructConsequenceData", { incoming: data }); + if (this.parent instanceof BladesRoll) { + this.csqData = U.objMerge({ + [RollResult.partial]: { + 0: { name: "", type: "", attribute: "" }, + 1: { name: "", type: "", attribute: "" }, + 2: { name: "", type: "", attribute: "" } + }, + [RollResult.fail]: { + 0: { name: "", type: "", attribute: "" }, + 1: { name: "", type: "", attribute: "" }, + 2: { name: "", type: "", attribute: "" } + } + }, this.parent.getFlagVal("consequenceData") ?? {}); + this._consequenceAI = new BladesAI(AGENTS.ConsequenceAdjuster); + } + } getData() { const data = super.getData(); + switch (this.dialogType) { + case BladesDialogType.Selection: return this.prepareSelectionData(data); + case BladesDialogType.Consequence: return this.prepareConsequenceData(data); + default: return null; + } + } + prepareSelectionData(data) { data.title = this.title; data.tabs = this.tabs; data.docType = this.docType; data.tags = this.tags; return data; } + prepareConsequenceData(data) { + eLog.checkLog3("dialog", "updateConsequenceDialog() this.csqData", this.csqData); + eLog.checkLog3("dialog", "prepareConsequenceData", { incoming: data }); + data.consequenceData = this.csqData; + data.consequenceTypeOptions = this.consequenceTypeOptions; + data.consequenceTypeOptionsAll = Object.entries(C.ConsequenceDisplay) + .map(([cType, cDisplay]) => ({ value: cType, display: cDisplay })); + data.consequenceAttributeOptions = [ + { value: AttributeTrait.insight, display: "Insight" }, + { value: AttributeTrait.prowess, display: "Prowess" }, + { value: AttributeTrait.resolve, display: "Resolve" } + ]; + eLog.checkLog3("dialog", "prepareConsequenceData", { outgoing: data }); + return data; + } + get consequenceTypeOptions() { + if (this.parent instanceof BladesRoll) { + return { + [RollResult.partial]: C.Consequences[this.parent.finalPosition][RollResult.partial] + .map((cType) => ({ value: cType, display: C.ConsequenceDisplay[cType] })), + [RollResult.fail]: C.Consequences[this.parent.finalPosition][RollResult.fail] + .map((cType) => ({ value: cType, display: C.ConsequenceDisplay[cType] })) + }; + } + return {}; + } + updateConsequenceDialog(html, isRendering = true) { + + + [RollResult.partial, RollResult.fail].forEach((rollResult) => { + for (let i = 0; i < 3; i++) { + const thisCsqData = { + type: html.find(`[data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.type"]`)[0].value, + attribute: html.find(`[data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.attribute"]`)[0].value, + name: html.find(`[data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.name"]`)[0].value, + resistOptions: this.csqData[rollResult][i] + .resistOptions ?? {} + }; + if (thisCsqData.type) { + thisCsqData.icon = C.ConsequenceIcons[thisCsqData.type]; + } + const resistOptionElems = Array.from(html.find(`input[type="text"][data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.resistOptions`)); + eLog.checkLog3("dialog", "...resistOptionElems", { html, resistOptionElems, thisCsqData }); + if (resistOptionElems.length > 0) { + for (let j = 0; j < resistOptionElems.length; j++) { + thisCsqData.resistOptions ??= {}; + thisCsqData.resistOptions[j] = { + name: resistOptionElems[i].value, + type: html.find(`[data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.resistOptions.${j}.type"]`)[0].value, + isSelected: html.find(`[data-flag-target="rollCollab.consequenceData.${rollResult}.${i}.resistOptions.${j}.isSelected"]`)[0].checked + }; + } + } + this.csqData[rollResult][i] = thisCsqData; + } + }); + eLog.checkLog3("dialog", "updateConsequenceDialog() this.csqData", this.csqData); + if (isRendering) { + this.render(); + } + } + async writeToRollInstance(html) { + if (this.parent instanceof BladesRoll) { + this.updateConsequenceDialog(html, false); + await this.parent.setFlagVal("consequenceData", this.csqData); + } + } + _consequenceAI; + async queryAI(event) { + if (!this._consequenceAI) { + this._consequenceAI = new BladesAI(AGENTS.ConsequenceAdjuster); + } + const dataAction = event.currentTarget.dataset.action; + if (dataAction && dataAction.startsWith("ai-query")) { + const [rollResult, csqIndex] = dataAction.split(/-/).slice(2); + const csqName = this.csqData[rollResult][csqIndex]?.name; + if (csqName) { + const response = await this._consequenceAI?.query(csqName, csqName); + if (response) { + this.addResistanceOptions(rollResult, csqIndex, response.split("|")); + } + } + } + } + async setFlagVal(target, value) { + if (this.parent instanceof BladesRoll) { + return this.parent.setFlagVal(target, value, false); + } + } + async addResistanceOptions(rollResult, cIndex, rOptions) { + const cData = this.csqData[rollResult][cIndex]; + if (!cData) { + return; + } + const cType = cData.type; + const rType = C.ResistedConsequenceTypes[cType] ?? undefined; + const resistOptions = {}; + for (let i = 0; i < rOptions.length; i++) { + resistOptions[i] = { + name: rOptions[i], + isSelected: false + }; + if (rType) { + resistOptions[i].type = rType; + resistOptions[i].icon = C.ConsequenceIcons[rType]; + } + } + this.csqData[rollResult][cIndex].resistOptions = resistOptions; + eLog.checkLog3("dialog", "addResistanceOptions() this.csqData", this.csqData); + this.render(); + } + async selectResistOption(event) { + eLog.checkLog3("dialog", "Clicked Resistance Option", event); + const dataAction = event.currentTarget.dataset.action; + if (dataAction && dataAction.startsWith("gm-select-toggle")) { + const [rollResult, csqIndex, resIndex] = dataAction.split(/-/).slice(3); + eLog.checkLog3("dialog", "... Action Passed", { rollResult, csqIndex, resIndex }); + const resOptions = this.csqData[rollResult][csqIndex].resistOptions ?? {}; + resOptions[resIndex].isSelected = !resOptions[resIndex].isSelected; + if (resOptions[resIndex].isSelected) { + Object.keys(resOptions) + .filter((key) => key !== resIndex) + .forEach((key) => { resOptions[key].isSelected = false; }); + } + this.csqData[rollResult][csqIndex].resistOptions = resOptions; + this.render(); + } + } activateListeners(html) { super.activateListeners(html); + ApplyTooltipListeners(html); + switch (this.dialogType) { + case BladesDialogType.Selection: + this.activateSelectionListeners(html); + break; + case BladesDialogType.Consequence: + this.activateConsequenceListeners(html); + break; + } + } + activateSelectionListeners(html) { const self = this; html.find(".nav-tabs .tab-selector").on("click", (event) => { const tabIndex = U.pInt($(event.currentTarget).data("tab")); - const numItems = Object.values(self.tabs)[tabIndex].length; + const numItems = Object.values(self.tabs ?? [])[tabIndex].length; const width = U.pInt(150 * Math.ceil(Math.sqrt(numItems))); eLog.checkLog3("nav", "Nav Tab Size Recalculation", { tabIndex, numItems, width }); this.render(false, { width }); }); - ApplyTooltipListeners(html); html.find("[data-item-id]").on("click", function () { if ($(this).parent().hasClass("locked")) { return; @@ -134,14 +345,22 @@ class BladesSelectorDialog extends Dialog { const docId = $(this).data("itemId"); const docType = $(this).data("docType"); eLog.checkLog("dialog", "[BladesDialog] on Click", { elem: this, docId, docType, parent: self.parent }); - if (docType === "Actor") { - self.parent.addSubActor(docId, self.tags); - } - else if (docType === "Item") { - self.parent.addSubItem(docId); + if (self.parent instanceof BladesActor) { + if (docType === "Actor") { + self.parent.addSubActor(docId, self.tags); + } + else if (docType === "Item") { + self.parent.addSubItem(docId); + } } self.close(); }); } + activateConsequenceListeners(html) { + html.find("input").on({ blur: () => this.updateConsequenceDialog(html) }); + html.find("select").on({ change: () => this.updateConsequenceDialog(html) }); + html.find('[data-action^="ai-query"]').on({ click: (event) => this.queryAI(event) }); + html.find('[data-action^="gm-select-toggle"]').on({ click: (event) => this.selectResistOption(event) }); + } } -export default BladesSelectorDialog; \ No newline at end of file +export default BladesDialog; \ No newline at end of file diff --git a/module/BladesRoll.js b/module/BladesRoll.js index 6cff9260..7b57faec 100644 --- a/module/BladesRoll.js +++ b/module/BladesRoll.js @@ -10,7 +10,7 @@ import C, { BladesActorType, BladesItemType, RollPermissions, RollType, RollSubT import { BladesActor, BladesPC, BladesCrew } from "./documents/BladesActorProxy.js"; import { BladesItem, BladesGMTracker } from "./documents/BladesItemProxy.js"; import { ApplyTooltipListeners } from "./core/gsap.js"; -import BladesAI, { AGENTS } from "./core/ai.js"; +import BladesDialog from "./BladesDialog.js"; function isRollType(str) { return typeof str === "string" && str in RollType; @@ -338,8 +338,21 @@ class BladesRollMod { PushCost0: () => this.rollInstance.isPushed(), Consequence: () => this.rollInstance.rollType === RollType.Resistance && Boolean(this.rollInstance.rollConsequence), - HarmLevel: () => this.rollInstance.rollType === RollType.Resistance - && [ConsequenceType.InsightHarm1, ConsequenceType.InsightHarm2, ConsequenceType.InsightHarm3, ConsequenceType.InsightHarm4, ConsequenceType.ProwessHarm1, ConsequenceType.ProwessHarm2, ConsequenceType.ProwessHarm3, ConsequenceType.ProwessHarm4, ConsequenceType.ResolveHarm1, ConsequenceType.ResolveHarm2, ConsequenceType.ResolveHarm3, ConsequenceType.ResolveHarm4].includes(this.rollInstance.rollConsequence?.type ?? ""), + HarmLevel: () => { + if (this.rollInstance.rollType !== RollType.Resistance) { + return false; + } + if (!this.rollInstance.rollConsequence?.type) { + return false; + } + const { type: csqType } = this.rollInstance.rollConsequence; + return [ + ConsequenceType.InsightHarm1, ConsequenceType.ProwessHarm1, ConsequenceType.ResolveHarm1, + ConsequenceType.InsightHarm2, ConsequenceType.ProwessHarm2, ConsequenceType.ResolveHarm2, + ConsequenceType.InsightHarm3, ConsequenceType.ProwessHarm3, ConsequenceType.ResolveHarm3, + ConsequenceType.InsightHarm4, ConsequenceType.ProwessHarm4, ConsequenceType.ResolveHarm4 + ].includes(csqType); + }, QualityPenalty: () => this.rollInstance.isTraitRelevant(Factor.quality) && (this.rollInstance.rollFactors.source[Factor.quality]?.value ?? 0) < (this.rollInstance.rollFactors.opposition[Factor.quality]?.value ?? 0), @@ -1728,33 +1741,9 @@ class BladesRoll extends DocumentSheet { } return null; } - async addConsequence(cData) { - eLog.checkLog2("rollCollab", "addConsequence", cData); - await this.setFlagVal(`consequenceData.${cData.name}`, cData); - } - async clearConsequence(cName) { - await this.clearFlagVal(`consequenceData.${cName}`); - } - async addResistanceOptions(cResult, cIndex, rNames) { - const cData = this.getFlagVal(`consequenceData.${cResult}.${cIndex}`); - eLog.checkLog2("rollCollab", "addResistanceOptions", { cResult, cIndex, rNames, cData }); - if (!cData) { - return; - } - const cType = cData.type; - const rType = C.ResistedConsequenceTypes[cType] ?? undefined; - const resistOptions = cData.resistOptions ?? {}; - for (const rName of rNames) { - resistOptions[rName] = { name: rName, isSelected: false }; - if (rType) { - resistOptions[rName].type = rType; - } - } - await this.setFlagVal(`consequenceData.${cResult}.${cIndex}.resistOptions`, resistOptions); - } - promptGMForConsequences() { + async applyConsequencesFromDialog(html) { } - + get finalPosition() { return Object.values(Position)[U.clampNum(Object.values(Position) .indexOf(this.initialPosition) @@ -2102,48 +2091,11 @@ class BladesRoll extends DocumentSheet { set rollMods(val) { this._rollMods = val; } - get consequenceTypeOptions() { - return { - [RollResult.partial]: C.Consequences[this.finalPosition][RollResult.partial] - .map((cType) => ({ value: cType, display: cType })), - [RollResult.fail]: C.Consequences[this.finalPosition][RollResult.fail] - .map((cType) => ({ value: cType, display: cType })) - }; - } - _consequenceAI; - async manageConsequenceAI(sData) { - const { consequenceData } = sData; - if (!consequenceData) { - return; - } - if (!this._consequenceAI) { - this._consequenceAI = new BladesAI(AGENTS.ConsequenceAdjuster); - } - await Promise.all(Object.entries(consequenceData) - .map(([rollResult, cResultData]) => Object.entries(cResultData) - .map(([cIndex, cData]) => { - if (!cData.resistOptions) { - if (!this._consequenceAI?.hasQueried(cData.name)) { - this._consequenceAI?.query(cData.name, cData.name); - } - else { - const response = this._consequenceAI?.getResponse(cData.name); - if (response) { - return this.addResistanceOptions(rollResult, cIndex, response.split("|")); - } - } - } - return undefined; - }))); - } async getData() { const context = super.getData(); this.initRollMods(this.getRollModsData()); this.rollMods.forEach((rollMod) => rollMod.applyRollModEffectKeys()); const sheetData = this.getSheetData(this.getIsGM(), this.getRollCosts()); - if (game.user.isGM && this.rollConsequences) { - this.manageConsequenceAI(sheetData); - } return { ...context, ...sheetData }; } getRollModsData() { @@ -2178,7 +2130,7 @@ class BladesRoll extends DocumentSheet { return rollCosts.find((costData) => costData.costType === "SpecialArmor"); } getSheetData(isGM, rollCosts) { - const { flagData: rData, rollPrimary, rollTraitData, rollTraitOptions, finalDicePool, finalPosition, finalEffect, finalResult, rollMods, rollFactors, consequenceTypeOptions } = this; + const { flagData: rData, rollPrimary, rollTraitData, rollTraitOptions, finalDicePool, finalPosition, finalEffect, finalResult, rollMods, rollFactors } = this; if (!rollPrimary) { throw new Error("A primary roll source is required for BladesRoll."); } @@ -2222,7 +2174,6 @@ class BladesRoll extends DocumentSheet { ...rollResultData, ...GMBoostsData, ...positionEffectTradeData, - consequenceTypeOptions, userPermission }; } @@ -2548,7 +2499,6 @@ class BladesRoll extends DocumentSheet { await this.roll.evaluate({ async: true }); if (this.isApplyingConsequences) { this.rollPhase = RollPhase.ApplyingConsequences; - this.promptGMForConsequences(); } eLog.checkLog3("rollCollab", "[resolveRoll()] After Evaluation, Before Chat", { roll: this, dieVals: this.dieVals }); await this.outputRollToChat(); @@ -2762,25 +2712,28 @@ class BladesRoll extends DocumentSheet { $(event.currentTarget).parents(".controls-panel").toggleClass("active"); } }); - html.find("[data-action=\"gm-set\"").on({ + html.find("[data-action=\"gm-set\"]").on({ click: this._gmControlSet.bind(this) }); - html.find("[data-action=\"gm-set-position\"").on({ + html.find("[data-action=\"gm-set-position\"]").on({ click: this._gmControlSetPosition.bind(this) }); - html.find("[data-action=\"gm-set-effect\"").on({ + html.find("[data-action=\"gm-set-effect\"]").on({ click: this._gmControlSetEffect.bind(this) }); - html.find("[data-action=\"gm-set-target\"").on({ + html.find("[data-action=\"gm-set-target\"]").on({ click: this._gmControlSetTargetToValue.bind(this), contextmenu: this._gmControlResetTarget.bind(this) }); - html.find("[data-action=\"gm-toggle-factor\"").on({ + html.find("[data-action=\"gm-toggle-factor\"]").on({ click: this._gmControlToggleFactor.bind(this) }); html .find("select[data-action='gm-select']") .on({ change: this._onSelectChange.bind(this) }); + html + .find("[data-action=\"gm-edit-consequences\"]") + .on({ click: () => BladesDialog.DisplayRollConsequenceDialog(this) }); html .find("[data-action='gm-text-input']") .on({ blur: this._onTextInputBlur.bind(this) }); diff --git a/module/blades.js b/module/blades.js index f29cc6a7..72370138 100644 --- a/module/blades.js +++ b/module/blades.js @@ -20,7 +20,7 @@ import BladesCrewSheet from "./sheets/actor/BladesCrewSheet.js"; import BladesNPCSheet from "./sheets/actor/BladesNPCSheet.js"; import BladesFactionSheet from "./sheets/actor/BladesFactionSheet.js"; import BladesRoll, { BladesRollMod, BladesRollPrimary, BladesRollOpposition, BladesRollParticipant } from "./BladesRoll.js"; -import BladesSelectorDialog from "./BladesDialog.js"; +import BladesDialog from "./BladesDialog.js"; import BladesAI, { AGENTS } from "./core/ai.js"; import BladesActiveEffect from "./BladesActiveEffect.js"; import BladesGMTrackerSheet from "./sheets/item/BladesGMTrackerSheet.js"; @@ -131,7 +131,7 @@ Hooks.once("init", async () => { BladesActiveEffect.Initialize(), BladesGMTrackerSheet.Initialize(), BladesScore.Initialize(), - BladesSelectorDialog.Initialize(), + BladesDialog.Initialize(), BladesClockKeeperSheet.Initialize(), BladesPushAlert.Initialize(), BladesRoll.Initialize(), @@ -153,7 +153,7 @@ Hooks.once("socketlib.ready", () => { function InitOverlaySockets() { setTimeout(() => { clockOverlayUp = clockOverlayUp || BladesClockKeeperSheet.InitSockets(); - pushControllerUp = clockOverlayUp || BladesPushAlert.InitSockets(); + pushControllerUp = pushControllerUp || BladesPushAlert.InitSockets(); if (clockOverlayUp && pushControllerUp) { return; } diff --git a/module/core/ai.js b/module/core/ai.js index 668b7d7c..7b1dc6f2 100644 --- a/module/core/ai.js +++ b/module/core/ai.js @@ -119,6 +119,7 @@ class BladesAI { fetchRequest.body = JSON.parse(fetchRequest.body); eLog.checkLog3("BladesAI", "AI Query", { prompt: fetchRequest, response: data }); this.responses[queryID] = data.choices[0].message.content; + return this.responses[queryID]; } } export const AGENTS = { @@ -144,12 +145,13 @@ export const AGENTS = { ] }, ConsequenceAdjuster: { - systemMessage: "You will act as a \"Setback Adjuster\" for a game of Blades In The Dark. You will be prompted with a short phrase describing an injury, lasting consequence or other setback. Your job is to respond with a pipe-delimited list of three possible alternative consequences that are less severe by one level, using the following scale as a rough guide: Level 1 = Lesser (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2 = Moderate (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3 = Severe (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4 = Fatal or Ruinous (e.g. 'Impaled Through Heart', 'Electrocuted', 'Headquarters Burned to the Ground'). So, if you determine that the consequence described in the prompt is severity level 3, you should respond with three narratively similar consequences that are severity level 2. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt.", + systemMessage: "You will act as a \"Setback Adjuster\" for a game of Blades In The Dark. You will be prompted with a short phrase describing an injury, lasting consequence or other setback. Your job is to respond with a pipe-delimited list of three possible alternative consequences that are less severe by one level, using the following scale as a rough guide: Level 1 = Lesser (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2 = Moderate (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3 = Severe (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4 = Fatal or Ruinous (e.g. 'Impaled Through Heart', 'Electrocuted', 'Headquarters Burned to the Ground'). So, if you determine that the consequence described in the prompt is severity level 3, you should respond with three narratively similar consequences that are severity level 2. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt. The consequences you suggest should always describe a NEGATIVE setback or complication, just one that is less severe than the one described in the prompt.", examplePrompts: [ { human: "Shattered Right Leg", ai: "Fractured Right Ankle|Dislocated Knee|Broken Foot" }, { human: "Soul Destroyed", ai: "Fully Corrupted|Lost In Darkness|Spirit Broken" }, { human: "Humiliated", ai: "Embarrassed|Momentarily Off-Balance|Enraged" }, - { human: "She Escapes!", ai: "She Spots a Means of Escape|She Puts More Distance Between You|She Stops to Gloat" } + { human: "She Escapes!", ai: "She Spots a Means of Escape|She Puts More Distance Between You|She Stops to Gloat" }, + { human: "The fire spreads to the hostages.", ai: "The fire approaches the hostages|The hostages must be evacuated|The fire billows choking black smoke." } ] } }; diff --git a/module/core/constants.js b/module/core/constants.js index 8ce86a45..e7d5382d 100644 --- a/module/core/constants.js +++ b/module/core/constants.js @@ -475,6 +475,49 @@ const C = { [ConsequenceType.ComplicationMajor]: ConsequenceType.ComplicationMinor, [ConsequenceType.ComplicationMinor]: ConsequenceType.None }, + ConsequenceDisplay: { + [ConsequenceType.ReducedEffect]: "Reduced Effect", + [ConsequenceType.ComplicationMinor]: "Minor Complication", + [ConsequenceType.ComplicationMajor]: "Major Complication", + [ConsequenceType.ComplicationSerious]: "Serious Complication", + [ConsequenceType.LostOpportunity]: "Lost Opportunity", + [ConsequenceType.WorsePosition]: "Worse Position", + [ConsequenceType.InsightHarm1]: "Harm 1 (Insight)", + [ConsequenceType.InsightHarm2]: "Harm 2 (Insight)", + [ConsequenceType.InsightHarm3]: "Harm 3 (Insight)", + [ConsequenceType.InsightHarm4]: "Harm 4 (Insight)", + [ConsequenceType.ProwessHarm1]: "Harm 1 (Prowess)", + [ConsequenceType.ProwessHarm2]: "Harm 2 (Prowess)", + [ConsequenceType.ProwessHarm3]: "Harm 3 (Prowess)", + [ConsequenceType.ProwessHarm4]: "Harm 4 (Prowess)", + [ConsequenceType.ResolveHarm1]: "Harm 1 (Resolve)", + [ConsequenceType.ResolveHarm2]: "Harm 2 (Resolve)", + [ConsequenceType.ResolveHarm3]: "Harm 3 (Resolve)", + [ConsequenceType.ResolveHarm4]: "Harm 4 (Resolve)", + [ConsequenceType.None]: "None" + }, + ConsequenceIcons: { + [ConsequenceType.ReducedEffect]: "systems/eunos-blades/assets/icons/consequence-icons/csq-reduced-effect.svg", + [ConsequenceType.ComplicationMinor]: "systems/eunos-blades/assets/icons/consequence-icons/csq-complication-minor.svg", + [ConsequenceType.ComplicationMajor]: "systems/eunos-blades/assets/icons/consequence-icons/csq-complication-major.svg", + [ConsequenceType.ComplicationSerious]: "systems/eunos-blades/assets/icons/consequence-icons/csq-complication-serious.svg", + [ConsequenceType.LostOpportunity]: "systems/eunos-blades/assets/icons/consequence-icons/csq-lost-opportunity.svg", + [ConsequenceType.WorsePosition]: "systems/eunos-blades/assets/icons/consequence-icons/csq-worse-position.svg", + [ConsequenceType.InsightHarm1]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-insight-1.svg", + [ConsequenceType.InsightHarm2]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-insight-2.svg", + [ConsequenceType.InsightHarm3]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-insight-3.svg", + [ConsequenceType.InsightHarm4]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-insight-4.svg", + [ConsequenceType.ProwessHarm1]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-prowess-1.svg", + [ConsequenceType.ProwessHarm2]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-prowess-2.svg", + [ConsequenceType.ProwessHarm3]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-prowess-3.svg", + [ConsequenceType.ProwessHarm4]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-prowess-4.svg", + [ConsequenceType.ResolveHarm1]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-resolve-1.svg", + [ConsequenceType.ResolveHarm2]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-resolve-2.svg", + [ConsequenceType.ResolveHarm3]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-resolve-3.svg", + [ConsequenceType.ResolveHarm4]: "systems/eunos-blades/assets/icons/consequence-icons/csq-harm-resolve-4.svg", + [ConsequenceType.None]: "" + }, + // Colors: { bWHITE: "rgba(255, 255, 255, 1)", WHITE: "rgba(200, 200, 200, 1)", diff --git a/module/sheets/actor/BladesActorSheet.js b/module/sheets/actor/BladesActorSheet.js index 13a34488..1116e847 100644 --- a/module/sheets/actor/BladesActorSheet.js +++ b/module/sheets/actor/BladesActorSheet.js @@ -12,7 +12,7 @@ import C, { BladesActorType, BladesItemType, AttributeTrait, ActionTrait, Factor import Tags from "../../core/tags.js"; import BladesActor from "../../BladesActor.js"; import BladesItem from "../../BladesItem.js"; -import BladesSelectorDialog from "../../BladesDialog.js"; +import BladesDialog from "../../BladesDialog.js"; import BladesActiveEffect from "../../BladesActiveEffect.js"; import BladesRoll, { BladesRollPrimary, BladesRollOpposition } from "../../BladesRoll.js"; class BladesActorSheet extends ActorSheet { @@ -323,7 +323,7 @@ class BladesActorSheet extends ActorSheet { if (!dialogDocs || !docCat || !docType) { return; } - await BladesSelectorDialog.DisplaySelectionDialog(this.actor, U.tCase(`Add ${docCat.replace(/_/g, " ")}`), docType, dialogDocs, docTags); + await BladesDialog.DisplaySelectionDialog(this.actor, U.tCase(`Add ${docCat.replace(/_/g, " ")}`), docType, dialogDocs, docTags); } async _onItemRemoveClick(event) { event.preventDefault(); diff --git a/scss/dialog/_dialogs.scss b/scss/dialog/_dialogs.scss index 95f02647..d580f51a 100644 --- a/scss/dialog/_dialogs.scss +++ b/scss/dialog/_dialogs.scss @@ -5,7 +5,7 @@ // max-height: 500px; height: auto !important; - // width: auto !important; + width: auto !important; --item-info-height: 100px; --buttons-height: 25px; @@ -181,5 +181,57 @@ height: var(--buttons-height); } + + .consequence-section { + width: 600px; + h2 { + + } + .roll-consequence-row { + .button-icon { + position: unset; + } + .roll-consequence-entry-row { + .consequence-type-icon { + width: 24px; + min-width: 24px; + filter: brightness(1.5) drop-shadow(0 0 4px black); + } + .roll-consequence-type-select { + font-family: Oswald; + flex-basis: 150px; + + } + .roll-consequence-attribute-select { + font-family: Oswald; + flex-basis: 95px; + } + .consequence-name { + background: rgba(0,0,0,0.5); + flex-basis: 375px; + } + } + .consequence-resist-options-container { + width: 90%; + margin-left: 10%; + justify-content: stretch; + + .consequence-resist-option { + width: 100%; + justify-content: stretch; + + .toggle-icon { + position: unset; + } + + .shadowed.consequence-name { + // flex-basis: 200px; + flex-grow: 1; + background: rgba(0, 0, 0, 0.5); + } + } + } + } + } } } \ No newline at end of file diff --git a/scss/sheets/_roll-collab-sheet.scss b/scss/sheets/_roll-collab-sheet.scss index d07472fc..dffc26ef 100644 --- a/scss/sheets/_roll-collab-sheet.scss +++ b/scss/sheets/_roll-collab-sheet.scss @@ -1,5 +1,7 @@ & { + --roll-spacing: 5px; + height: auto !important; width: unset !important; @@ -144,8 +146,7 @@ width: auto; max-width: 175px; margin: 0; - // left: calc(-50% - 3px); - right: calc(100% + 3px); + right: calc(100% + 2*var(--roll-spacing, 5px) - 2px); background: rgb(24, 24, 24); top: 0px; padding: 2px 0 2px 5px; @@ -699,8 +700,6 @@ &.gm-roll-collab { .window-content form { - min-width: var(--full-roll-width, 550px); - max-width: var(--full-roll-width, 550px); &.roll-type-action { --full-roll-width: 775px; @@ -903,6 +902,8 @@ form { + min-width: var(--full-roll-width, 550px); + max-width: var(--full-roll-width, 550px); // Awesome Fire Shader --- https://codepen.io/eunomiac/pen/OJQeMPr?editors=1010 // Great Nav Tabs --- https://codepen.io/abdelRhman345/pen/PXMmdv // Single Flame Shader --- https://codepen.io/osublake/pen/pqNXoq @@ -916,13 +917,13 @@ &.roll-type-action { - min-width: 775px; - max-width: 775px; + // min-width: 775px; + // max-width: 775px; } &.roll-type-resistance { - min-width: 500px; - max-width: 500px; + // min-width: 500px; + // max-width: 500px; .sheet-root { border-radius: 30px; @@ -1386,11 +1387,14 @@ position: relative; gap: 5px 5px; + &.roll-participants-block { border: none } + &.rolling-block { min-height: 62px; .roll-sheet-sub-block.roll-header-block { + margin-top: -2px; .roll-sheet-select, .roll-readonly { @@ -1428,6 +1432,9 @@ align-items: stretch; position: static; + gap: var(--roll-spacing, 5px);; + padding: var(--roll-spacing, 5px); + .split-root-left, .split-root-right, .full-root-container { @@ -1436,20 +1443,21 @@ z-index: 5; &.split-root-left { - flex-basis: 75%; - max-width: 370px; + flex-basis: 85%; + max-width: 85%; + // max-width: 370px; } &.split-root-right { - flex-basis: 35%; + flex-basis: 15%; display: flex; align-items: stretch; - justify-content: stretch; + justify-content: space-between; flex-direction: column; z-index: 4; section.sheet-main { - flex-grow: 1; + // flex-grow: 1; z-index: 0; overflow: hidden; @@ -1619,6 +1627,12 @@ width: 100%; } + .roll-sheet-sub-block.roll-effects-block.inactive-mod-block { + padding-right: 40px; + margin-right: -40px; + z-index: -2; + } + .roll-sheet-float-block { &.roll-effects-block { @@ -1627,13 +1641,10 @@ position: absolute; pointer-events: none; margin-top: 20px; - - &:not(.inactive-mod-block) { - flex-direction: column; - width: unset; - align-items: flex-start; - gap: 0; - } + flex-direction: column; + width: unset; + align-items: flex-start; + gap: 0; } &.roll-button { @@ -1742,7 +1753,7 @@ position: absolute; translate: -50% 100%; left: 50%; - // top: 25%; + top: 50%; font-family: var(--font-emphasis); color: var(--blades-white); text-shadow: diff --git a/templates/components/button-icon.hbs b/templates/components/button-icon.hbs index 084a2d7f..ec6a3670 100644 --- a/templates/components/button-icon.hbs +++ b/templates/components/button-icon.hbs @@ -1,5 +1,13 @@ - + {{#if buttonClass}} + + {{/if}} + {{#if buttonImg}} + + {{/if}} + {{#if buttonLabel}} + + {{/if}} {{#if tooltip}} {{/if}} diff --git a/templates/components/toggle-icon.hbs b/templates/components/toggle-icon.hbs index 6103e6f2..96115661 100644 --- a/templates/components/toggle-icon.hbs +++ b/templates/components/toggle-icon.hbs @@ -2,11 +2,16 @@ {{#if targetKey}} {{else if targetFlagKey}} - + + {{else if dataAction}} + {{/if}} - - + {{#if isToggled}} + + {{else}} + + {{/if}} {{#if tooltip}} + {{/if}}{{/unless}}{{/unless}} \ No newline at end of file diff --git a/templates/dialog-consequence.hbs b/templates/dialog-consequence.hbs index e08afa89..6bdde877 100644 --- a/templates/dialog-consequence.hbs +++ b/templates/dialog-consequence.hbs @@ -1,51 +1,169 @@ - {{/if}} @@ -20,4 +25,8 @@ {{/unless}} {{/if}} + + {{#unless targetKey}}{{#unless targetFlagKey}}{{#if dataAction}} +