Skip to content

Commit 554ee01

Browse files
authored
Merge pull request #19 from mordachai/soundfx
Soundfx
2 parents 5e21987 + af05de5 commit 554ee01

11 files changed

Lines changed: 573 additions & 58 deletions

File tree

scripts/apps/drawing-sheet.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MODULE_ID, PIN_COLORS } from "../config.js";
22
import { collaborativeUpdate } from "../utils/socket-handler.js";
3+
import { applyTapeEffectToSound } from "../utils/audio-utils.js";
34
import {
45
startConnectionAnimation,
56
stopConnectionAnimation,
@@ -67,6 +68,7 @@ export class CustomDrawingSheet extends DrawingConfig {
6768
context.connections = customData.connections;
6869
context.font = customData.font;
6970
context.fontSize = customData.fontSize;
71+
context.audioEffectEnabled = customData.audioEffectEnabled;
7072

7173
// Enrich the linked object for display
7274
context.enrichedLinkedObject = context.linkedObject ? await TextEditor.enrichHTML(context.linkedObject, { async: true }) : "";
@@ -111,6 +113,7 @@ export class CustomDrawingSheet extends DrawingConfig {
111113
noteType: noteType,
112114
text: this.document.flags[MODULE_ID]?.text || "Default Text",
113115
audioPath: this.document.flags[MODULE_ID]?.audioPath || "",
116+
audioEffectEnabled: this.document.flags[MODULE_ID]?.audioEffectEnabled !== false, // Default to true
114117
linkedObject: this.document.flags[MODULE_ID]?.linkedObject || "",
115118
image: this.document.flags[MODULE_ID]?.image || (noteType === "handout" ? "modules/investigation-board/assets/newhandout.webp" : "modules/investigation-board/assets/placeholder.webp"),
116119
identityName: this.document.flags[MODULE_ID]?.identityName || "",
@@ -230,27 +233,38 @@ export class CustomDrawingSheet extends DrawingConfig {
230233
this.previewSound.stop();
231234
this.previewSound = null;
232235
previewAudioBtn.innerHTML = '<i class="fas fa-play"></i>';
236+
previewAudioBtn.title = "Preview Audio";
233237
return;
234238
}
235239

236240
const audioPath = this.element.querySelector("input[name='audioPath']")?.value;
237241
if (audioPath) {
238242
this.previewSound = await game.audio.play(audioPath, { volume: 0.8 });
243+
244+
// Apply tape effect if enabled in the form
245+
const effectEnabled = this.element.querySelector("input[name='audioEffectEnabled']")?.checked;
246+
if (effectEnabled && this.previewSound) {
247+
applyTapeEffectToSound(this.previewSound);
248+
}
249+
239250
previewAudioBtn.innerHTML = '<i class="fas fa-stop"></i>';
251+
previewAudioBtn.title = "Stop Preview";
240252

241253
// Reset button when sound ends
242254
const sound = this.previewSound;
243255
setTimeout(() => {
244256
if (this.previewSound === sound && !sound.playing) {
245257
previewAudioBtn.innerHTML = '<i class="fas fa-play"></i>';
258+
previewAudioBtn.title = "Preview Audio";
246259
}
247260
}, 100); // Small delay to let it start
248261

249262
// Monitor for end
250263
const checkEnd = setInterval(() => {
251264
if (!this.previewSound || this.previewSound !== sound || !sound.playing) {
252265
if (this.previewSound === sound) {
253-
previewAudioBtn.innerHTML = '<i class="fas fa-play"></i> Preview Audio';
266+
previewAudioBtn.innerHTML = '<i class="fas fa-play"></i>';
267+
previewAudioBtn.title = "Preview Audio";
254268
this.previewSound = null;
255269
}
256270
clearInterval(checkEnd);
@@ -389,6 +403,10 @@ export class CustomDrawingSheet extends DrawingConfig {
389403
updates[`flags.${MODULE_ID}.audioPath`] = data.audioPath;
390404
}
391405

406+
if (noteType === "media") {
407+
updates[`flags.${MODULE_ID}.audioEffectEnabled`] = !!data.audioEffectEnabled;
408+
}
409+
392410
if (data.linkedObject !== undefined) {
393411
updates[`flags.${MODULE_ID}.linkedObject`] = data.linkedObject;
394412
}

scripts/apps/note-previewer.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MODULE_ID, SOCKET_NAME } from "../config.js";
22
import { socket, activeGlobalSounds } from "../utils/socket-handler.js";
3+
import { applyTapeEffectToElement, applyTapeEffectToSound } from "../utils/audio-utils.js";
34

45
// v13 namespaced imports
56
const ApplicationV2 = foundry.applications.api.ApplicationV2;
@@ -92,6 +93,12 @@ export class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
9293
const cassette = html.querySelector(".cassette-wrapper");
9394

9495
if (localAudio && cassette) {
96+
// Apply tape effect if enabled
97+
const noteData = this.document.flags[MODULE_ID];
98+
if (noteData?.audioEffectEnabled !== false) {
99+
this.tapeEffect = applyTapeEffectToElement(localAudio);
100+
}
101+
95102
localAudio.addEventListener("play", () => {
96103
cassette.classList.add("playing");
97104
});
@@ -134,6 +141,9 @@ export class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
134141
const icon = playGlobalBtn.querySelector("i");
135142
const span = playGlobalBtn.querySelector("span");
136143

144+
const noteData = this.document.flags[MODULE_ID];
145+
const audioEffectEnabled = noteData?.audioEffectEnabled !== false;
146+
137147
if (this.globalSoundActive) {
138148
socket.emit(SOCKET_NAME, { action: "stopAudio", audioPath: audioPath });
139149
const localGlobal = activeGlobalSounds.get(audioPath);
@@ -149,11 +159,20 @@ export class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
149159
return;
150160
}
151161

152-
socket.emit(SOCKET_NAME, { action: "playAudio", audioPath: audioPath });
162+
socket.emit(SOCKET_NAME, {
163+
action: "playAudio",
164+
audioPath: audioPath,
165+
applyEffect: audioEffectEnabled
166+
});
153167

154168
const sound = await game.audio.play(audioPath, { volume: 0.8 });
155169
if (sound) {
156170
activeGlobalSounds.set(audioPath, sound);
171+
172+
if (audioEffectEnabled) {
173+
applyTapeEffectToSound(sound);
174+
}
175+
157176
this.globalSoundActive = true;
158177
icon.className = "fas fa-stop";
159178
span.innerText = "Stop for All";
@@ -224,6 +243,10 @@ export class NotePreviewer extends HandlebarsApplicationMixin(ApplicationV2) {
224243
}
225244

226245
async _onClose(options) {
246+
if (this.tapeEffect) {
247+
this.tapeEffect.disconnect();
248+
this.tapeEffect = null;
249+
}
227250
if (this.audioPollInterval) {
228251
clearInterval(this.audioPollInterval);
229252
this.audioPollInterval = null;

scripts/apps/setup-warning.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { MODULE_ID } from "../config.js";
2+
3+
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
4+
5+
export class SetupWarningDialog extends HandlebarsApplicationMixin(ApplicationV2) {
6+
static DEFAULT_OPTIONS = {
7+
id: "ib-setup-warning",
8+
tag: "div",
9+
classes: ["ib-setup-warning-app"],
10+
window: {
11+
title: "Investigation Board: Setup Recommended",
12+
resizable: false,
13+
minimizable: false,
14+
},
15+
position: {
16+
width: 500,
17+
height: "auto"
18+
}
19+
};
20+
21+
static PARTS = {
22+
content: {
23+
template: "modules/investigation-board/templates/setup-warning.html"
24+
}
25+
};
26+
27+
async _prepareContext(options) {
28+
const users = game.users.filter(u => !u.isGM);
29+
const playerRoles = [...new Set(users.map(u => u.role))];
30+
if (playerRoles.length === 0) playerRoles.push(1);
31+
32+
const drawingPerm = playerRoles.every(role => game.permissions.DRAWING_CREATE.includes(role));
33+
const browsePerm = playerRoles.every(role => game.permissions.FILES_BROWSE.includes(role));
34+
const uploadPerm = playerRoles.every(role => game.permissions.FILES_UPLOAD.includes(role));
35+
36+
const roleNames = {
37+
1: "Player",
38+
2: "Trusted Player",
39+
3: "Assistant GM"
40+
};
41+
42+
const missingUsers = users.map(u => {
43+
const canDraw = game.permissions.DRAWING_CREATE.includes(u.role);
44+
const canBrowse = game.permissions.FILES_BROWSE.includes(u.role);
45+
const canUpload = game.permissions.FILES_UPLOAD.includes(u.role);
46+
47+
return {
48+
name: u.name,
49+
roleName: roleNames[u.role] || "Unknown",
50+
canDraw,
51+
canBrowse,
52+
canUpload
53+
};
54+
});
55+
56+
return {
57+
drawingPerm,
58+
browsePerm,
59+
uploadPerm,
60+
missingUsers
61+
};
62+
}
63+
64+
_onRender(context, options) {
65+
const html = this.element;
66+
67+
html.querySelector('[data-action="ok"]')?.addEventListener("click", () => {
68+
this.close();
69+
});
70+
71+
html.querySelector('[data-action="disable"]')?.addEventListener("click", async () => {
72+
await game.settings.set(MODULE_ID, "showSetupWarning", false);
73+
this.close();
74+
});
75+
}
76+
}

scripts/canvas/custom-drawing.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -730,13 +730,48 @@ export class CustomDrawing extends Drawing {
730730
const texture = await PIXI.Assets.load(imagePath);
731731
if (texture && this.photoImageSprite) {
732732
this.photoImageSprite.texture = texture;
733-
this.photoImageSprite.width = fullWidth * 0.9;
734-
this.photoImageSprite.height = fullHeight * 0.9;
735-
this.photoImageSprite.position.set(fullWidth * 0.05, fullHeight * 0.05);
736733

737-
// Ensure mask is disabled for futuristic mode
738-
this.photoImageSprite.mask = null;
739-
if (this.photoMask) this.photoMask.visible = false;
734+
const frameWidth = fullWidth * 0.9;
735+
const frameHeight = fullHeight * 0.9;
736+
const frameX = fullWidth * 0.05;
737+
const frameY = fullHeight * 0.05;
738+
739+
// Use center-top positioning logic to avoid stretching
740+
const textureRatio = texture.width / texture.height;
741+
const frameRatio = frameWidth / frameHeight;
742+
743+
let spriteWidth, spriteHeight, offsetX, offsetY;
744+
745+
if (textureRatio > frameRatio) {
746+
// Case 1: Image is wider than frame - Fit by height, center horizontally
747+
spriteHeight = frameHeight;
748+
spriteWidth = frameHeight * textureRatio;
749+
offsetX = (frameWidth - spriteWidth) / 2;
750+
offsetY = 0;
751+
} else {
752+
// Case 2: Image is taller than frame - Fit by width, top aligned
753+
spriteWidth = frameWidth;
754+
spriteHeight = frameWidth / textureRatio;
755+
offsetX = 0;
756+
offsetY = 0;
757+
}
758+
759+
this.photoImageSprite.width = spriteWidth;
760+
this.photoImageSprite.height = spriteHeight;
761+
this.photoImageSprite.position.set(frameX + offsetX, frameY + offsetY);
762+
763+
// Apply Mask to clip overflow in futuristic mode too
764+
if (!this.photoMask) {
765+
this.photoMask = new PIXI.Graphics();
766+
this.addChild(this.photoMask);
767+
}
768+
this.photoMask.clear();
769+
this.photoMask.beginFill(0xffffff);
770+
this.photoMask.drawRect(frameX, frameY, frameWidth, frameHeight);
771+
this.photoMask.endFill();
772+
this.photoImageSprite.mask = this.photoMask;
773+
this.photoMask.visible = true;
774+
this.photoImageSprite.visible = true;
740775
}
741776
} catch (err) {
742777
console.error(`Failed to load user photo: ${noteData.image}`, err);

scripts/main.js

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
clearConnectionPreview
1212
} from "./canvas/connection-manager.js";
1313
import { initSocket, socket, collaborativeUpdate } from "./utils/socket-handler.js";
14+
import { SetupWarningDialog } from "./apps/setup-warning.js";
1415
import {
1516
createNote,
1617
createPhotoNoteFromActor,
@@ -255,36 +256,16 @@ Hooks.once("ready", () => {
255256

256257
// Show setup warning to GM if enabled
257258
if (game.user.isGM && game.settings.get(MODULE_ID, "showSetupWarning")) {
258-
const drawingPerm = game.permissions.DRAWING_CREATE.includes(1); // PLAYER role
259-
const filePerm = game.permissions.FILES_BROWSE.includes(1); // PLAYER role
259+
const users = game.users.filter(u => !u.isGM);
260+
const playerRoles = [...new Set(users.map(u => u.role))];
261+
if (playerRoles.length === 0) playerRoles.push(1);
262+
263+
const drawingPerm = playerRoles.every(role => game.permissions.DRAWING_CREATE.includes(role));
264+
const browsePerm = playerRoles.every(role => game.permissions.FILES_BROWSE.includes(role));
265+
const uploadPerm = playerRoles.every(role => game.permissions.FILES_UPLOAD.includes(role));
260266

261-
if (!drawingPerm || !filePerm) {
262-
const { DialogV2 } = foundry.applications.api;
263-
new DialogV2({
264-
window: { title: "Investigation Board: Setup Recommended" },
265-
content: `
266-
<p>To allow <strong>Players</strong> to fully use the Investigation Board, consider updating these World Permissions in <b>Game Settings >> User Management >> Configure Permissions</b>:</p>
267-
<ul>
268-
<li><strong>Use Drawing Tools</strong>: ${drawingPerm ? "✅ Enabled" : "❌ Disabled (Needed to create/manipulate notes and connections)"}</li>
269-
<li><strong>Upload Files</strong>: ${filePerm ? "✅ Enabled" : "❌ Disabled (Needed to create Photo and Handout notes with images)"}</li>
270-
</ul>
271-
<p style="color: #882222; font-style: italic;"><strong>Security Note:</strong> Enabling 'Upload Files' for players gives them access to your server's file system through the File Picker. Be careful with who you let access your files!</p>
272-
<hr>
273-
`,
274-
buttons: [
275-
{
276-
action: "ok",
277-
label: "Understood",
278-
icon: "fas fa-check"
279-
},
280-
{
281-
action: "disable",
282-
label: "Don't show again",
283-
icon: "fas fa-times",
284-
callback: (event, button, dialog) => game.settings.set(MODULE_ID, "showSetupWarning", false)
285-
}
286-
]
287-
}).render(true);
267+
if (!drawingPerm || !browsePerm || !uploadPerm) {
268+
new SetupWarningDialog().render(true);
288269
}
289270
}
290271
});
@@ -295,11 +276,30 @@ Hooks.on("createDrawing", (drawing, options, userId) => {
295276
const noteData = drawing.flags[MODULE_ID];
296277
if (!noteData) return;
297278

279+
// Determine who should see the sheet: either the direct userId or the original requester via socket
280+
const requesterId = options.ibRequestingUser || userId;
281+
298282
// If this is the user who created the note, open the edit dialog
299-
if (userId === game.user.id && !options.skipAutoOpen) {
283+
if (requesterId === game.user.id && !options.skipAutoOpen) {
300284
setTimeout(() => {
301-
drawing.sheet.render(true);
302-
}, 150);
285+
// Calculate screen position to place dialog below the note
286+
const drawingObject = canvas.drawings.get(drawing.id);
287+
if (drawingObject) {
288+
const bounds = drawingObject.getBounds();
289+
// bounds are in screen coordinates (pixels)
290+
const top = bounds.bottom + 20; // 20px padding below the note
291+
const left = bounds.left + (bounds.width / 2) - 200; // Center horizontally (sheet width is 400)
292+
293+
drawing.sheet.render(true, {
294+
position: {
295+
top: Math.max(0, top),
296+
left: Math.max(0, left)
297+
}
298+
});
299+
} else {
300+
drawing.sheet.render(true);
301+
}
302+
}, 250);
303303
}
304304

305305
// If we're in Investigation Board mode, refresh interactivity after the drawing is rendered
@@ -373,6 +373,11 @@ Hooks.on("updateDrawing", async (drawing, changes, options, userId) => {
373373
// If flags changed, refresh the drawing to update visuals on ALL clients
374374
if (flagsChanged && placeable) {
375375
await placeable.refresh();
376+
377+
// Also re-render open NotePreviewer for this drawing
378+
const appId = `note-preview-${drawing.id}`;
379+
const app = foundry.applications.instances.get(appId);
380+
if (app) app.render();
376381
}
377382

378383
// Redraw connection lines when position OR connections change

0 commit comments

Comments
 (0)