mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 21:38:39 +00:00
472 lines
16 KiB
JavaScript
472 lines
16 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(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 };
|