Files
vtt_work/investigation-board_v13/investigation-board/scripts/investigation-board.js
T

472 lines
17 KiB
JavaScript

//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(element);
element.querySelectorAll(".file-picker-button").forEach(btn => btn.addEventListener("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 = Array.isArray(controls) ? controls.find((c) => c.name === "notes") : (controls["notes"] ?? controls.notes);
if (!journalControls) return;
journalControls.tools.push(
{ name: "createStickyNote", title: "Create Sticky Note", icon: "fa-solid fa-note-sticky", onChange: (ev, active) => { if (active) createNote("sticky"); }, button: true },
{ name: "createPhotoNote", title: "Create Photo Note", icon: "fa-solid fa-camera-polaroid", onChange: (ev, active) => { if (active) createNote("photo"); }, button: true },
{ name: "createIndexCard", title: "Create Index Card", icon: "fa-regular fa-rectangle-list", onChange: (ev, active) => { if (active) 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 };