sync: foundry v0.2
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "investigation-board",
|
||||
"title": "Investigation Board",
|
||||
"description": "A Foundry VTT module that lets everyone create...photo notes on the scene as collaborative investigation tools.",
|
||||
"version": "1.3.2",
|
||||
"authors": [{ "name": "Gus", "url": "", "email": "" }],
|
||||
"compatibility": { "minimum": "12", "verified": "13" },
|
||||
"esmodules": [
|
||||
"scripts/settings.js",
|
||||
"scripts/investigation-board.js"
|
||||
],
|
||||
"styles": ["styles/style.css"],
|
||||
"packs": [],
|
||||
"templates": ["templates/drawing-sheet.html"],
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
//investigation-boards.js
|
||||
|
||||
import { registerSettings } from "./settings.js";
|
||||
|
||||
const MODULE_ID = "investigation-board";
|
||||
const BASE_FONT_SIZE = 15;
|
||||
const PIN_COLORS = ["redPin.webp", "bluePin.webp", "yellowPin.webp", "greenPin.webp"];
|
||||
|
||||
function getBaseCharacterLimits() {
|
||||
return game.settings.get(MODULE_ID, "baseCharacterLimits") || {
|
||||
sticky: 60,
|
||||
photo: 15,
|
||||
index: 200,
|
||||
};
|
||||
}
|
||||
|
||||
function getDynamicCharacterLimits(noteType, currentFontSize) {
|
||||
const baseLimits = getBaseCharacterLimits();
|
||||
const scaleFactor = BASE_FONT_SIZE / currentFontSize;
|
||||
const limits = baseLimits[noteType] || { sticky: 60, photo: 15, index: 200 };
|
||||
return {
|
||||
sticky: Math.round(limits.sticky * scaleFactor),
|
||||
photo: Math.round(limits.photo * scaleFactor),
|
||||
index: Math.round(limits.index * scaleFactor),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class CustomDrawingSheet extends DrawingConfig {
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["custom-drawing-sheet"],
|
||||
template: "modules/investigation-board/templates/drawing-sheet.html",
|
||||
width: 400,
|
||||
height: "auto",
|
||||
title: "Note Configuration",
|
||||
});
|
||||
}
|
||||
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
data.noteType = this.object.flags[MODULE_ID]?.type || "sticky";
|
||||
data.text = this.object.flags[MODULE_ID]?.text || "Default Text";
|
||||
data.image = this.object.flags[MODULE_ID]?.image || "modules/investigation-board/assets/placeholder.webp";
|
||||
|
||||
// Pass along the extra identityName for futuristic photo notes
|
||||
data.identityName = this.object.flags[MODULE_ID]?.identityName || "";
|
||||
|
||||
// Include the board mode from settings for conditional display in the template
|
||||
data.boardMode = game.settings.get(MODULE_ID, "boardMode");
|
||||
|
||||
data.noteTypes = {
|
||||
sticky: "Sticky Note",
|
||||
photo: "Photo Note",
|
||||
index: "Index Card",
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
const updates = {
|
||||
[`flags.${MODULE_ID}.type`]: formData.noteType,
|
||||
[`flags.${MODULE_ID}.text`]: formData.text,
|
||||
[`flags.${MODULE_ID}.image`]: formData.image || "modules/investigation-board/assets/placeholder.webp",
|
||||
};
|
||||
if (formData.identityName !== undefined) {
|
||||
updates[`flags.${MODULE_ID}.identityName`] = formData.identityName;
|
||||
}
|
||||
|
||||
|
||||
await this.object.update(updates);
|
||||
const drawing = canvas.drawings.get(this.object.id);
|
||||
if (drawing) drawing.refresh();
|
||||
}
|
||||
|
||||
// Add activateListeners to hook up the file-picker button.
|
||||
activateListeners(element) {
|
||||
super.activateListeners(html);
|
||||
html.find(".file-picker-button").click(ev => {
|
||||
ev.preventDefault();
|
||||
// Open Foundry's FilePicker for images; adjust the "current" directory if needed.
|
||||
new FilePicker({
|
||||
type: "image",
|
||||
current: "modules/investigation-board/assets/",
|
||||
callback: path => {
|
||||
// Update the readonly input with the chosen image path.
|
||||
const input = element.querySelector("input[name='image']"); if (input) input.value = path;
|
||||
}
|
||||
}).browse();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class CustomDrawing extends Drawing {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.bgSprite = null;
|
||||
this.pinSprite = null;
|
||||
this.noteText = null;
|
||||
this.photoImageSprite = null;
|
||||
this.identityNameText = null;
|
||||
this.futuristicText = null;
|
||||
}
|
||||
|
||||
// Ensure sprites are created when the drawing is first rendered.
|
||||
async draw() {
|
||||
await super.draw();
|
||||
await this._updateSprites();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Ensure sprites update correctly on refresh.
|
||||
async refresh() {
|
||||
await super.refresh();
|
||||
await this._updateSprites();
|
||||
return this;
|
||||
}
|
||||
|
||||
async _updateSprites() {
|
||||
const noteData = this.document.flags[MODULE_ID];
|
||||
if (!noteData) return;
|
||||
|
||||
const isPhoto = noteData.type === "photo";
|
||||
const isIndex = noteData.type === "index";
|
||||
const mode = game.settings.get(MODULE_ID, "boardMode");
|
||||
|
||||
// FUTURISTIC PHOTO NOTE LAYOUT
|
||||
if (isPhoto && mode === "futuristic") {
|
||||
const fullWidth = game.settings.get(MODULE_ID, "photoNoteWidth");
|
||||
const margin = 10;
|
||||
const photoImgWidth = fullWidth * 0.4;
|
||||
const photoImgHeight = photoImgWidth * (4 / 3);
|
||||
const textAreaX = margin + photoImgWidth + margin;
|
||||
const fullHeight = photoImgHeight + margin * 2;
|
||||
|
||||
// --- Background Frame ---
|
||||
if (!this.bgSprite) {
|
||||
this.bgSprite = new PIXI.Sprite();
|
||||
this.addChildAt(this.bgSprite, 0);
|
||||
}
|
||||
try {
|
||||
// Always use the fixed photo frame image.
|
||||
this.bgSprite.texture = PIXI.Texture.from("modules/investigation-board/assets/photoFrame.webp");
|
||||
} catch (err) {
|
||||
console.error("Failed to load photo frame texture", err);
|
||||
this.bgSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
this.bgSprite.width = fullWidth;
|
||||
this.bgSprite.height = fullHeight;
|
||||
|
||||
// --- Foreground (User-Assigned) Photo ---
|
||||
if (!this.photoImageSprite) {
|
||||
this.photoImageSprite = new PIXI.Sprite();
|
||||
this.addChild(this.photoImageSprite);
|
||||
}
|
||||
try {
|
||||
// Use a fallback if no image is assigned.
|
||||
const imagePath = noteData.image || "modules/investigation-board/assets/placeholder.webp";
|
||||
this.photoImageSprite.texture = PIXI.Texture.from(imagePath);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load user photo: ${noteData.image}`, err);
|
||||
this.photoImageSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
// Position the user photo inside the frame.
|
||||
this.photoImageSprite.width = fullWidth * 0.9;
|
||||
this.photoImageSprite.height = fullHeight * 0.9;
|
||||
this.photoImageSprite.position.set(fullWidth * 0.05, fullHeight * 0.05);
|
||||
|
||||
// --- Identity Name and Additional Text (Futuristic) ---
|
||||
const font = game.settings.get(MODULE_ID, "font");
|
||||
const baseFontSize = game.settings.get(MODULE_ID, "baseFontSize");
|
||||
const fontSize = (fullWidth / 200) * baseFontSize;
|
||||
const textStyle = new PIXI.TextStyle({
|
||||
fontFamily: font,
|
||||
fontSize: fontSize,
|
||||
fill: "#000000",
|
||||
wordWrap: true,
|
||||
wordWrapWidth: fullWidth - textAreaX - margin,
|
||||
align: "left",
|
||||
});
|
||||
if (!this.identityNameText) {
|
||||
this.identityNameText = new PIXI.Text("", textStyle);
|
||||
this.addChild(this.identityNameText);
|
||||
}
|
||||
this.identityNameText.text = noteData.identityName || "Name";
|
||||
this.identityNameText.style = textStyle;
|
||||
this.identityNameText.position.set(textAreaX, margin);
|
||||
|
||||
if (!this.futuristicText) {
|
||||
this.futuristicText = new PIXI.Text("", textStyle);
|
||||
this.addChild(this.futuristicText);
|
||||
}
|
||||
this.futuristicText.text = noteData.text || "";
|
||||
this.futuristicText.style = textStyle;
|
||||
this.futuristicText.position.set(textAreaX, margin + this.identityNameText.height + 5);
|
||||
|
||||
// Remove default note text if present.
|
||||
if (this.noteText) {
|
||||
this.removeChild(this.noteText);
|
||||
this.noteText.destroy();
|
||||
this.noteText = null;
|
||||
}
|
||||
|
||||
// --- Pin Handling (Futuristic) ---
|
||||
const pinSetting = game.settings.get(MODULE_ID, "pinColor");
|
||||
if (pinSetting === "none") {
|
||||
if (this.pinSprite) {
|
||||
this.removeChild(this.pinSprite);
|
||||
this.pinSprite.destroy();
|
||||
this.pinSprite = null;
|
||||
}
|
||||
} else {
|
||||
if (!this.pinSprite) {
|
||||
this.pinSprite = new PIXI.Sprite();
|
||||
this.addChild(this.pinSprite);
|
||||
}
|
||||
let pinColor = noteData.pinColor;
|
||||
if (!pinColor) {
|
||||
pinColor = (pinSetting === "random")
|
||||
? PIN_COLORS[Math.floor(Math.random() * PIN_COLORS.length)]
|
||||
: `${pinSetting}Pin.webp`;
|
||||
if (this.document.isOwner) {
|
||||
await this.document.update({ [`flags.${MODULE_ID}.pinColor`]: pinColor });
|
||||
}
|
||||
}
|
||||
const pinImage = `modules/investigation-board/assets/${pinColor}`;
|
||||
try {
|
||||
this.pinSprite.texture = PIXI.Texture.from(pinImage);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load pin texture: ${pinImage}`, err);
|
||||
this.pinSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
this.pinSprite.width = 40;
|
||||
this.pinSprite.height = 40;
|
||||
this.pinSprite.position.set(fullWidth / 2 - 20, 3);
|
||||
}
|
||||
return; // End early for futuristic photo notes.
|
||||
}
|
||||
|
||||
// STANDARD LAYOUT (Modern photo notes, sticky, index, etc.)
|
||||
const width = isPhoto
|
||||
? game.settings.get(MODULE_ID, "photoNoteWidth")
|
||||
: isIndex
|
||||
? game.settings.get(MODULE_ID, "indexNoteWidth") || 600
|
||||
: game.settings.get(MODULE_ID, "stickyNoteWidth");
|
||||
|
||||
const height = isPhoto
|
||||
? Math.round(width / (225 / 290))
|
||||
: isIndex
|
||||
? Math.round(width / (600 / 400))
|
||||
: width;
|
||||
|
||||
// Background Image based on board mode.
|
||||
const getBackgroundImage = (noteType, mode) => {
|
||||
if (mode === "futuristic") {
|
||||
if (noteType === "photo") return "modules/investigation-board/assets/futuristic_photoFrame.webp";
|
||||
if (noteType === "index") return "modules/investigation-board/assets/futuristic_note_index.webp";
|
||||
return "modules/investigation-board/assets/futuristic_note_white.webp";
|
||||
} else if (mode === "custom") {
|
||||
if (noteType === "photo") return "modules/investigation-board/assets/custom_photoFrame.webp";
|
||||
if (noteType === "index") return "modules/investigation-board/assets/custom_note_index.webp";
|
||||
return "modules/investigation-board/assets/custom_note_white.webp";
|
||||
}
|
||||
// Default "modern" mode:
|
||||
if (noteType === "photo") return "modules/investigation-board/assets/photoFrame.webp";
|
||||
if (noteType === "index") return "modules/investigation-board/assets/note_index.webp";
|
||||
return "modules/investigation-board/assets/note_white.webp";
|
||||
};
|
||||
const bgImage = getBackgroundImage(noteData.type, mode);
|
||||
|
||||
if (!this.bgSprite) {
|
||||
this.bgSprite = new PIXI.Sprite();
|
||||
this.addChild(this.bgSprite);
|
||||
}
|
||||
try {
|
||||
this.bgSprite.texture = PIXI.Texture.from(bgImage);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load background texture: ${bgImage}`, err);
|
||||
this.bgSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
this.bgSprite.width = width;
|
||||
this.bgSprite.height = height;
|
||||
|
||||
// --- Foreground (User-Assigned) Photo for Modern Mode ---
|
||||
// (This is the code missing from your current version.)
|
||||
if (isPhoto) {
|
||||
const fgImage = noteData.image || "modules/investigation-board/assets/placeholder.webp";
|
||||
if (!this.photoImageSprite) {
|
||||
this.photoImageSprite = new PIXI.Sprite();
|
||||
this.addChild(this.photoImageSprite);
|
||||
}
|
||||
try {
|
||||
this.photoImageSprite.texture = PIXI.Texture.from(fgImage);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load foreground texture: ${fgImage}`, err);
|
||||
this.photoImageSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
// Use offsets similar to your old code.
|
||||
const widthOffset = width * 0.13333;
|
||||
const heightOffset = height * 0.30246;
|
||||
this.photoImageSprite.width = width - widthOffset;
|
||||
this.photoImageSprite.height = height - heightOffset;
|
||||
this.photoImageSprite.position.set(widthOffset / 2, heightOffset / 2);
|
||||
this.photoImageSprite.visible = true;
|
||||
} else if (this.photoImageSprite) {
|
||||
this.photoImageSprite.visible = false;
|
||||
}
|
||||
|
||||
// --- Pin Handling (Standard) ---
|
||||
{
|
||||
const pinSetting = game.settings.get(MODULE_ID, "pinColor");
|
||||
if (pinSetting === "none") {
|
||||
if (this.pinSprite) {
|
||||
this.removeChild(this.pinSprite);
|
||||
this.pinSprite.destroy();
|
||||
this.pinSprite = null;
|
||||
}
|
||||
} else {
|
||||
if (!this.pinSprite) {
|
||||
this.pinSprite = new PIXI.Sprite();
|
||||
this.addChild(this.pinSprite);
|
||||
}
|
||||
let pinColor = noteData.pinColor;
|
||||
if (!pinColor) {
|
||||
pinColor = (pinSetting === "random")
|
||||
? PIN_COLORS[Math.floor(Math.random() * PIN_COLORS.length)]
|
||||
: `${pinSetting}Pin.webp`;
|
||||
if (this.document.isOwner) {
|
||||
await this.document.update({ [`flags.${MODULE_ID}.pinColor`]: pinColor });
|
||||
}
|
||||
}
|
||||
const pinImage = `modules/investigation-board/assets/${pinColor}`;
|
||||
try {
|
||||
this.pinSprite.texture = PIXI.Texture.from(pinImage);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load pin texture: ${pinImage}`, err);
|
||||
this.pinSprite.texture = PIXI.Texture.EMPTY;
|
||||
}
|
||||
this.pinSprite.width = 40;
|
||||
this.pinSprite.height = 40;
|
||||
this.pinSprite.position.set(width / 2 - 20, 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Default text layout for non-futuristic notes.
|
||||
const font = game.settings.get(MODULE_ID, "font");
|
||||
const baseFontSize = game.settings.get(MODULE_ID, "baseFontSize");
|
||||
const fontSize = (width / 200) * baseFontSize;
|
||||
const textStyle = new PIXI.TextStyle({
|
||||
fontFamily: font,
|
||||
fontSize: fontSize,
|
||||
fill: "#000000",
|
||||
wordWrap: true,
|
||||
wordWrapWidth: width - 15,
|
||||
align: "center",
|
||||
});
|
||||
const truncatedText = this._truncateText(noteData.text || "Default Text", font, noteData.type, fontSize);
|
||||
if (!this.noteText) {
|
||||
this.noteText = new PIXI.Text(truncatedText, textStyle);
|
||||
this.noteText.anchor.set(0.5);
|
||||
this.addChild(this.noteText);
|
||||
} else {
|
||||
this.noteText.style = textStyle;
|
||||
this.noteText.text = truncatedText;
|
||||
}
|
||||
this.noteText.position.set(width / 2, isPhoto ? height - 25 : height / 2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
_truncateText(text, font, noteType, currentFontSize) {
|
||||
const limits = getDynamicCharacterLimits(font, currentFontSize);
|
||||
const charLimit = limits[noteType] || 100;
|
||||
return text.length <= charLimit ? text : text.slice(0, charLimit).trim() + "...";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function createNote(noteType) {
|
||||
const scene = canvas.scene;
|
||||
if (!scene) {
|
||||
console.error("Cannot create note: No active scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve width settings (or use defaults)
|
||||
const stickyW = game.settings.get(MODULE_ID, "stickyNoteWidth") || 200;
|
||||
const photoW = game.settings.get(MODULE_ID, "photoNoteWidth") || 225;
|
||||
const indexW = game.settings.get(MODULE_ID, "indexNoteWidth") || 600;
|
||||
|
||||
const width = noteType === "photo" ? photoW
|
||||
: noteType === "index" ? indexW
|
||||
: stickyW;
|
||||
const height = noteType === "photo" ? Math.round(photoW / (225 / 290))
|
||||
: noteType === "index" ? Math.round(indexW / (600 / 400))
|
||||
: stickyW;
|
||||
|
||||
const dims = canvas.dimensions;
|
||||
const x = dims.width / 2;
|
||||
const y = dims.height / 2;
|
||||
|
||||
// Get default text from settings (fallback if missing)
|
||||
const defaultText = game.settings.get(MODULE_ID, `${noteType}NoteDefaultText`) || "Notes";
|
||||
|
||||
// Determine board mode and include identityName if note is a futuristic photo note
|
||||
const boardMode = game.settings.get(MODULE_ID, "boardMode");
|
||||
const extraFlags = {};
|
||||
if (noteType === "photo" && boardMode === "futuristic") {
|
||||
extraFlags.identityName = "";
|
||||
}
|
||||
|
||||
await canvas.scene.createEmbeddedDocuments("Drawing", [
|
||||
{
|
||||
type: "r",
|
||||
author: game.user.id,
|
||||
x,
|
||||
y,
|
||||
shape: { width, height },
|
||||
fillColor: "#ffffff",
|
||||
fillAlpha: 1,
|
||||
strokeColor: "transparent",
|
||||
strokeAlpha: 0,
|
||||
locked: false,
|
||||
flags: {
|
||||
[MODULE_ID]: {
|
||||
type: noteType,
|
||||
text: defaultText,
|
||||
...extraFlags
|
||||
},
|
||||
},
|
||||
"flags.core.sheetClass": "investigation-board.CustomDrawingSheet",
|
||||
"ownership": { default: 3 },
|
||||
},
|
||||
]);
|
||||
|
||||
canvas.drawings.activate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
Hooks.on("getSceneControlButtons", (controls) => {
|
||||
const journalControls = controls.find((c) => c.name === "notes");
|
||||
if (!journalControls) return;
|
||||
|
||||
journalControls.tools.push(
|
||||
{ name: "createStickyNote", title: "Create Sticky Note", icon: "fas fa-sticky-note", onClick: () => createNote("sticky"), button: true },
|
||||
{ name: "createPhotoNote", title: "Create Photo Note", icon: "fa-solid fa-camera-polaroid", onClick: () => createNote("photo"), button: true },
|
||||
{ name: "createIndexCard", title: "Create Index Card", icon: "fa-regular fa-subtitles", onClick: () => createNote("index"), button: true }
|
||||
);
|
||||
});
|
||||
|
||||
Hooks.once("init", () => {
|
||||
registerSettings();
|
||||
CONFIG.Drawing.objectClass = CustomDrawing;
|
||||
|
||||
DocumentSheetConfig.registerSheet(DrawingDocument, "investigation-board", CustomDrawingSheet, {
|
||||
label: "Note Drawing Sheet",
|
||||
types: ["base"],
|
||||
makeDefault: false,
|
||||
});
|
||||
|
||||
console.log("Investigation Board module initialized.");
|
||||
});
|
||||
|
||||
|
||||
export { CustomDrawing, CustomDrawingSheet };
|
||||
@@ -0,0 +1,167 @@
|
||||
export const MODULE_ID = "investigation-board";
|
||||
|
||||
export const registerSettings = function() {
|
||||
|
||||
const refreshAllDrawings = () => {
|
||||
if (canvas.drawings) {
|
||||
canvas.drawings.placeables.forEach(drawing => {
|
||||
if (drawing.document.flags[MODULE_ID]) {
|
||||
drawing.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update the pinColor setting to include a "No Pins" option.
|
||||
game.settings.register(MODULE_ID, "pinColor", {
|
||||
name: "Pin Color",
|
||||
hint: "Choose the color of the pin for notes. Selecting 'Random' will randomly assign a pin color. Select 'No Pins' to disable pin display.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
choices: {
|
||||
random: "Random",
|
||||
red: "Red",
|
||||
blue: "Blue",
|
||||
yellow: "Yellow",
|
||||
green: "Green",
|
||||
none: "No Pins"
|
||||
},
|
||||
default: "random",
|
||||
onChange: () => {
|
||||
if (canvas.drawings) {
|
||||
canvas.drawings.placeables.forEach(drawing => drawing.refresh());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register a new setting for board mode selection.
|
||||
game.settings.register(MODULE_ID, "boardMode", {
|
||||
name: "Board Mode",
|
||||
hint: "Select the board mode to change the styling of notes.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
choices: {
|
||||
modern: "Modern",
|
||||
futuristic: "Futuristic",
|
||||
custom: "Custom"
|
||||
},
|
||||
default: "modern",
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
|
||||
// Register existing settings
|
||||
game.settings.register(MODULE_ID, "stickyNoteWidth", {
|
||||
name: "Sticky Note Width",
|
||||
hint: "The width (in pixels) for all newly created sticky notes (default: 200).",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: 200,
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "photoNoteWidth", {
|
||||
name: "Photo Note Width",
|
||||
hint: "The width (in pixels) for all newly created photo notes (default: 225).",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: 225,
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "indexNoteWidth", {
|
||||
name: "Index Note Width",
|
||||
hint: "The width (in pixels) for all newly created index cards (default: 600).",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: 600,
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "baseFontSize", {
|
||||
name: "Base Font Size",
|
||||
hint: "The font size (in pixels) for text when the note width is at its default size (default: 16).",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: 16,
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "font", {
|
||||
name: "Font",
|
||||
hint: "Choose the font to be used in notes.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
choices: {
|
||||
"Rock Salt": "Rock Salt",
|
||||
"Courier New": "Courier New",
|
||||
"Times New Roman": "Times New Roman",
|
||||
"Signika": "Signika",
|
||||
"Arial": "Arial"
|
||||
},
|
||||
default: "Rock Salt",
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "characterNameKey", {
|
||||
name: "Character Name Key",
|
||||
hint: "Specify the key path to retrieve the name (e.g., 'prototypeToken.name' or 'system.alias' for Blades in the Dark). If empty, defaults to 'name'.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "prototypeToken.name",
|
||||
type: String,
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "stickyNoteDefaultText", {
|
||||
name: "Default Sticky Note Text",
|
||||
hint: "The default text to use for new sticky notes.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "Clue",
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "photoNoteDefaultText", {
|
||||
name: "Default Photo Note Text",
|
||||
hint: "The default text to use for new photo notes.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "Suspect/Place",
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
game.settings.register(MODULE_ID, "indexNoteDefaultText", {
|
||||
name: "Default Index Note Text",
|
||||
hint: "The default text to use for new index notes.",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "Notes",
|
||||
onChange: () => refreshAllDrawings()
|
||||
});
|
||||
|
||||
// Register base font size and character limits
|
||||
game.settings.register(MODULE_ID, "baseCharacterLimits", {
|
||||
name: "Base Character Limits",
|
||||
hint: "The base character limits for each font and note type. Edit this JSON to customize.",
|
||||
scope: "world",
|
||||
config: false, // Hidden from the settings UI
|
||||
type: Object,
|
||||
default: {
|
||||
"Rock Salt": { sticky: 90, photo: 20, index: 210 },
|
||||
"Courier New": { sticky: 250, photo: 30, index: 580 },
|
||||
"Times New Roman": { sticky: 200, photo: 30, index: 800 },
|
||||
"Signika": { sticky: 200, photo: 30, index: 650 },
|
||||
"Arial": { sticky: 200, photo: 30, index: 650 }
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
@font-face {
|
||||
font-family: 'Rock Salt';
|
||||
src: url('../assets/fonts/rock_salt.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.custom-drawing-text {
|
||||
font-family: 'Rock Salt', Arial, sans-serif;
|
||||
color: #000000; /* Black text */
|
||||
}
|
||||
|
||||
.sticky-note {
|
||||
background-image: url("modules/investigation-board/assets/note_white.webp");
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.photo-note {
|
||||
background-image: url("modules/investigation-board/assets/photoFrame.webp");
|
||||
width: 250px;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
/* EDIT FORM */
|
||||
/*
|
||||
.note-config-wrapper {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
min-height: 310px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.note-config-wrapper.sticky-mode {
|
||||
background-image: url("../assets/note_white.webp");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.note-config-wrapper.photo-mode {
|
||||
background-image: url("../assets/photoFrame.webp");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.note-config-wrapper .window-content {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.note-config-wrapper form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-type-group {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.note-text-group {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.note-config-wrapper.sticky-mode .note-text-group {
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.note-config-wrapper.photo-mode .note-text-group {
|
||||
top: 220px;
|
||||
}
|
||||
|
||||
.note-image-group {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.note-save-group {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.note-config-wrapper input,
|
||||
.note-config-wrapper textarea,
|
||||
.note-config-wrapper select {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
font-size: 14px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.note-config-wrapper label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.file-picker-button {
|
||||
margin-left: 5px;
|
||||
background: #eee;
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background: #3f51b5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.save-button i {
|
||||
margin-right: 5px;
|
||||
} */
|
||||
@@ -0,0 +1,37 @@
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="text">Note Text:</label>
|
||||
<textarea name="text" rows="4">{{text}}</textarea>
|
||||
</div>
|
||||
|
||||
{{!-- Only show the Image Path field for photo notes --}}
|
||||
{{#if (eq noteType "photo")}}
|
||||
<div class="form-group" style="display: block;">
|
||||
<label for="image">Image Path:</label>
|
||||
<div class="file-picker">
|
||||
<input type="text" name="image" value="{{image}}" placeholder="Enter image path" readonly />
|
||||
<button type="button" class="file-picker-button">
|
||||
<i class="fas fa-folder-open"></i> Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="form-group" style="display: none;"></div>
|
||||
{{/if}}
|
||||
|
||||
{{!-- If boardMode is futuristic and noteType is photo, show the extra Identity Name field --}}
|
||||
{{#if (eq noteType "photo")}}
|
||||
{{#if (eq boardMode "futuristic")}}
|
||||
<div class="form-group" style="display: block;">
|
||||
<label for="identityName">Identity Name:</label>
|
||||
<input type="text" name="identityName" value="{{identityName}}" placeholder="Enter identity name" />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="save-button">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||