Skip to content

Commit fc14271

Browse files
authored
Improve codex entry sharing (#969)
* Move the clock prompt to the progress pipeline file * Get the sheet extensions system working! * Get token instancing working! * Get the place tile function working!
1 parent 5310480 commit fc14271

File tree

16 files changed

+416
-65
lines changed

16 files changed

+416
-65
lines changed

lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,6 +1941,7 @@
19411941
"BrowseImage": "Browse Image",
19421942
"UploadClipboardImage": "Upload Clipboard Image",
19431943
"CodexUploadDirectory": "Codex Upload Directory",
1944+
"InstantiateToken": "Instantiate Token",
19441945
"CodexUploadDirectoryMissing": "You must set an user directory for codex clipboard images to be uploaded to. This can be configured in the system settings.",
19451946
"Scope": "Scope",
19461947
"Flag": "Flag",

module/documents/items/common/progress-data-model.mjs

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { systemPath } from '../../../helpers/config.mjs';
21
import { ObjectUtils } from '../../../helpers/object-utils.mjs';
32
import { MathHelper } from '../../../helpers/math-helper.mjs';
43
import FoundryUtils from '../../../helpers/foundry-utils.mjs';
@@ -292,39 +291,6 @@ export class ProgressDataModel extends foundry.abstract.DataModel {
292291
CommonEvents.progress(document, newTrack, 'add');
293292
}
294293

295-
/**
296-
* @param {Document} document
297-
* @param {String} propertyPath
298-
* @pararm {Boolean} selectStyle
299-
* @returns {Promise<void>}
300-
*/
301-
static async promptAddToDocument(document, propertyPath, selectStyle = false) {
302-
const result = await foundry.applications.api.DialogV2.input({
303-
window: { title: game.i18n.localize('FU.ClockAdd') },
304-
classes: ['projectfu', 'unique-dialog', 'backgroundstyle'],
305-
content: await foundry.applications.handlebars.renderTemplate(systemPath('templates/dialog/dialog-add-track.hbs'), {
306-
selectStyle: selectStyle,
307-
}),
308-
rejectClose: false,
309-
ok: {
310-
label: game.i18n.localize('FU.Confirm'),
311-
},
312-
});
313-
314-
if (result) {
315-
if (!result.name) {
316-
return;
317-
}
318-
console.log('Creating progress track with name: ', result.name);
319-
const newTrack = ProgressDataModel.construct(result.name, {
320-
id: result.id,
321-
max: result.max,
322-
style: result.style,
323-
});
324-
await this.addToDocument(document, propertyPath, newTrack);
325-
}
326-
}
327-
328294
/**
329295
* @description Calculates the change in a clock due to the result of a check
330296
* @param result

module/helpers/foundry-utils.mjs

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const { api, fields, handlebars } = foundry.applications;
1515
* @property {boolean} [disabled]
1616
* @property {boolean} [selected]
1717
* @property {boolean} [rule]
18+
* @property {String} img (Custom for PFU dialogs)
1819
* @property {Record<string, string>} [dataset]
1920
*/
2021

@@ -85,19 +86,21 @@ export default class FoundryUtils {
8586
/**
8687
* @param {String} title
8788
* @param content
89+
* @param {Object} options
8890
* @returns {Promise<*>}
8991
*/
90-
static async input(title, content) {
91-
const result = await foundry.applications.api.DialogV2.input({
92+
static async input(title, content, options = {}) {
93+
let defaultOptions = {
9294
window: { title: title, icon: 'fas fa-comment' },
9395
content: content,
9496
classes: ['projectfu', 'sheet', 'backgroundstyle', 'fu-dialog'],
9597
rejectClose: false,
9698
ok: {
9799
label: 'FU.Confirm',
98100
},
99-
});
100-
return result;
101+
};
102+
ObjectUtils.mergeRecursive(defaultOptions, options);
103+
return await foundry.applications.api.DialogV2.input(defaultOptions);
101104
}
102105

103106
/**
@@ -662,10 +665,12 @@ export default class FoundryUtils {
662665
*
663666
* @param html
664667
* @param className
668+
* @param eventName
665669
* @param {ContextMenuEntry[]} entries
666670
*/
667-
static contextMenu(html, className, entries) {
671+
static contextMenu(html, className, entries, eventName = 'click') {
668672
new foundry.applications.ux.ContextMenu(html, className, entries, {
673+
eventName: eventName,
669674
fixed: true,
670675
jQuery: false,
671676
});
@@ -790,4 +795,149 @@ export default class FoundryUtils {
790795
}
791796

792797
static PLACEHOLDER_IMG = 'icons/svg/mystery-man.svg';
798+
799+
/**
800+
* @desc Instantiates an actor on the current scene, prompting the user to click where to place it.
801+
* @param {FUActor} actor
802+
* @param {Object} data
803+
* @returns {TokenDocument|null}
804+
*/
805+
static async instantiateActor(actor, data) {
806+
const scene = game.scenes.viewed;
807+
if (!scene) {
808+
return null;
809+
}
810+
811+
const gridSize = scene.grid.size;
812+
const tokenData = await actor.getTokenDocument({
813+
x: 0,
814+
y: 0,
815+
actorLink: false,
816+
});
817+
const tokenSize = tokenData.width;
818+
819+
const notification = ui.notifications.info('Left click to place the token on the active scene, right click to cancel the operation.', { permanent: true });
820+
821+
const position = await new Promise((resolve) => {
822+
const clickHandler = (event) => {
823+
const { x, y } = event.getLocalPosition(canvas.stage);
824+
cleanup();
825+
resolve({ x, y });
826+
};
827+
828+
const rightClickHandler = () => {
829+
cleanup();
830+
ui.notifications.warn('Cancelled token placement.');
831+
resolve(null);
832+
};
833+
834+
const cleanup = () => {
835+
canvas.stage.off('click', clickHandler);
836+
canvas.stage.off('rightclick', rightClickHandler);
837+
ui.notifications.remove(notification);
838+
};
839+
840+
canvas.stage.on('click', clickHandler);
841+
canvas.stage.on('rightclick', rightClickHandler);
842+
});
843+
844+
if (!position) {
845+
return null; // cancelled
846+
}
847+
848+
const snapped = scene.grid.getSnappedPoint({
849+
x: position.x - (tokenSize * gridSize) / 2,
850+
y: position.y - (tokenSize * gridSize) / 2,
851+
});
852+
853+
const [tokenDocument] = await scene.createEmbeddedDocuments('Token', [
854+
{
855+
...tokenData.toObject(),
856+
x: snapped.x,
857+
y: snapped.y,
858+
...data,
859+
},
860+
]);
861+
return tokenDocument;
862+
}
863+
864+
/**
865+
* @param imagePath
866+
* @returns {Promise<Tile>}
867+
*/
868+
static async placeTile(imagePath) {
869+
const scene = game.scenes.viewed;
870+
871+
// Get image dimensions to use as default tile size
872+
// eslint-disable-next-line no-undef
873+
const tex = await loadTexture(imagePath);
874+
let width, height;
875+
876+
const promptTitle = `${StringUtils.localize('CONTROLS.TilePlace')} - Preset`;
877+
const preset = await FoundryUtils.selectOptionDialog(promptTitle, [
878+
{
879+
label: StringUtils.localizeMultiple(['Token', 'Scale']),
880+
value: 'token',
881+
},
882+
{
883+
label: StringUtils.localizeMultiple(['Default', 'Scale']),
884+
value: 'default',
885+
},
886+
]);
887+
switch (preset) {
888+
case 'token':
889+
{
890+
const scale = Math.min(100 / tex.width, 100 / tex.height);
891+
width = tex.width * scale;
892+
height = tex.height * scale;
893+
}
894+
break;
895+
case 'default':
896+
width = tex.width;
897+
height = tex.height;
898+
break;
899+
}
900+
901+
if (!preset) {
902+
return;
903+
}
904+
905+
const notification = ui.notifications.info('Left click to place tile on the active scene, right click to cancel the operation.', { permanent: true });
906+
907+
return new Promise((resolve) => {
908+
const clickHandler = async (event) => {
909+
const { x, y } = event.getLocalPosition(canvas.stage);
910+
911+
cleanup();
912+
913+
const [tileDocument] = await scene.createEmbeddedDocuments('Tile', [
914+
{
915+
texture: { src: imagePath },
916+
width,
917+
height,
918+
x: x - width / 2,
919+
y: y - height / 2,
920+
},
921+
]);
922+
923+
canvas.tiles.releaseAll();
924+
resolve(tileDocument);
925+
};
926+
927+
const rightClickHandler = () => {
928+
cleanup();
929+
ui.notifications.warn('Cancelled tile placement.');
930+
resolve(null); // cancelled
931+
};
932+
933+
const cleanup = () => {
934+
canvas.stage.off('click', clickHandler);
935+
canvas.stage.off('rightclick', rightClickHandler);
936+
ui.notifications.remove(notification);
937+
};
938+
939+
canvas.stage.on('click', clickHandler);
940+
canvas.stage.on('rightclick', rightClickHandler);
941+
});
942+
}
793943
}

module/helpers/object-utils.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @param {Object} target
33
* @param {Object} source
44
* @returns {(Object|boolean)[]}
5+
* @remarks If you don't care about whether it was changed, you can ignore the result.
56
*/
67
function mergeRecursive(target, source) {
78
let changed = false;

module/helpers/string-utils.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ function localize(key, data = undefined) {
6666
return key.toString();
6767
}
6868

69+
/**
70+
* @param {String[]} keys
71+
* @param {String} separator
72+
* @returns {String}
73+
*/
74+
function localizeMultiple(keys, separator = ' ') {
75+
return keys.map((k) => localize(k)).join(separator);
76+
}
77+
6978
/**
7079
* @param {String} key
7180
* @returns {Boolean}
@@ -139,6 +148,7 @@ export const StringUtils = Object.freeze({
139148
camelToKebab,
140149
titleToKebab,
141150
localize,
151+
localizeMultiple,
142152
hasLocalization,
143153
capitalize,
144154
truncate,

module/hooks.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ export const FUHooks = {
248248
* @remarks Uses {@link FeatureEvent}
249249
*/
250250
FEATURE_EVENT: `projectfu.events.feature`,
251-
251+
/**
252+
* // TODO: ERICA LOOK AT ME!
253+
*/
252254
GET_SIDEBAR_TOOLS: `projectfu.getSidebarTools`,
255+
/**
256+
* @description Dispatched when the party sheet is about to open, in order to provide options.
257+
*/
258+
SHEET_EXTENSIONS: `projectfu.sheets.extensions`,
253259
};

module/pipelines/progress-pipeline.mjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CheckPrompt } from '../checks/check-prompt.mjs';
1313
import { Checks } from '../checks/checks.mjs';
1414
import { CheckConfiguration } from '../checks/check-configuration.mjs';
1515
import FoundryUtils from '../helpers/foundry-utils.mjs';
16+
import { systemPath } from '../helpers/config.mjs';
1617

1718
/**
1819
* @param {FUActor} actor
@@ -188,6 +189,34 @@ async function openTrackMenuAtIndex(event, document, propertyPath, index) {
188189
});
189190
}
190191

192+
/**
193+
* @param {Document} document
194+
* @param {String} propertyPath
195+
* @pararm {Boolean} selectStyle
196+
* @returns {Promise<void>}
197+
*/
198+
async function promptAddToDocument(document, propertyPath, selectStyle = false) {
199+
const title = StringUtils.localize('FU.ClockAdd');
200+
const content = await foundry.applications.handlebars.renderTemplate(systemPath('templates/dialog/dialog-add-track.hbs'), {
201+
selectStyle: selectStyle,
202+
});
203+
const result = await FoundryUtils.input(title, content, {});
204+
205+
if (result) {
206+
if (!result.name) {
207+
ui.notifications.error(`No name was given for the tracker!`);
208+
return;
209+
}
210+
console.log('Creating progress track with name: ', result.name);
211+
const newTrack = ProgressDataModel.construct(result.name, {
212+
id: result.id,
213+
max: result.max,
214+
style: result.style,
215+
});
216+
await this.addToDocument(document, propertyPath, newTrack);
217+
}
218+
}
219+
191220
/**
192221
* @param {ChatMessage} message
193222
* @param {HTMLElement} html
@@ -224,4 +253,5 @@ export const ProgressPipeline = Object.freeze({
224253
getAdvanceTargetedAction,
225254
promptCheckAtIndexForDocument,
226255
openTrackMenuAtIndex,
256+
promptAddToDocument,
227257
});

module/projectfu.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import { FUChatLog } from './ui/chat-log.mjs';
115115
import { AutomationPipeline } from './pipelines/automation.mjs';
116116
import { Themes } from './ui/themes/theme-options.mjs';
117117
import { FUSidebar, FUSidebarApplication } from './ui/sidebar.mjs';
118+
import { SheetExtensions } from './sheets/sheet-extension.mjs';
118119

119120
globalThis.projectfu = {
120121
ClassFeatureDataModel,
@@ -175,6 +176,7 @@ Hooks.once('init', async () => {
175176
return FUPartySheet;
176177
},
177178
index: CompendiumIndex.instance,
179+
hooks: FUHooks,
178180
};
179181

180182
// (!) Data Models: Moved here due to lexical declaration issues otherwise
@@ -213,6 +215,11 @@ Hooks.once('init', async () => {
213215
ruleTrigger: FU.ruleTriggerRegistry,
214216
rulePredicate: FU.rulePredicateRegistry,
215217
};
218+
219+
FU.sheetExtensions = {
220+
party: new SheetExtensions(),
221+
};
222+
216223
CONFIG.FU = FU;
217224

218225
/**
@@ -425,6 +432,9 @@ Hooks.once('init', async () => {
425432
CompendiumBrowser.initialize();
426433
Themes.initialize();
427434

435+
// Fetch any extensions from modules
436+
Hooks.callAll(FUHooks.SHEET_EXTENSIONS, FU.sheetExtensions);
437+
428438
// Preload Handlebars templates.
429439
return preloadHandlebarsTemplates();
430440
});

0 commit comments

Comments
 (0)