import { MODULE_ID } from "../debug-mode.js"; import { HelperFunctions, HelperFunctions as HF } from "./HelperFunctions.js"; import { ImageDisplayManager } from "./ImageDisplayManager.js"; /** * This class manages the Art and Bounding Tiles, creating them, showing them in the Config, and * getting and setting their values */ export class ArtTileManager { /** * * @param {String} oldTileID - the id of a tile that's now missing * @param {String newTileID - the id of a new tile we're linking it to */ static async updateTileDataID(oldTileID, newTileID) { //this is an array of objects let tileDataArray = await ArtTileManager.getSceneSlideshowTiles("", false); //find object with id let index = tileDataArray.findIndex((tileData) => tileData.id === oldTileID); let tileObject = tileDataArray[index]; //.find((tileData) => tileData.id === oldTileID); tileObject = { ...tileObject, id: newTileID }; //here we ensure that the info for the linked tiles are also getting updated. Find ones that match our old ID, and update them to our new id //replace the object at its original index with the object w/ the new id if (tileObject && index !== undefined) { tileDataArray.splice(index, 1, tileObject); if (tileObject.isBoundingTile) { ArtTileManager.updateLinkedArtTiles(oldTileID, newTileID, tileDataArray); } await ArtTileManager.updateAllSceneTileFlags(tileDataArray); } } static async updateLinkedArtTiles(oldFrameID, newFrameID, tileDataArray) { //get art tiles that had us as their bounding tile let linkedArtTiles = []; let updatedLinkedArtTiles = []; linkedArtTiles = tileDataArray.filter( (tileData) => tileData.linkedBoundingTile === oldFrameID ); //update them with our new ID updatedLinkedArtTiles = linkedArtTiles.map((tileData) => { return { ...tileData, linkedBoundingTile: newFrameID, }; }); updatedLinkedArtTiles.forEach((atData) => { let atIndex = tileDataArray.findIndex((item) => item.id === atData.id); tileDataArray.splice(atIndex, 1, atData); }); return tileDataArray; } static async getDefaultData(isBoundingTile, linkedBoundingTile = "") { //determine its default name based on whether it's a bounding or display tile let displayName = isBoundingTile ? "frameTile" : "displayTile"; displayName = await ArtTileManager.incrementTileDisplayName(displayName); //increment it if one already exists with that name return { displayName: `${displayName}`, isBoundingTile: isBoundingTile, linkedBoundingTile: linkedBoundingTile, }; } static async incrementTileDisplayName(name) { let finalName = name; let tileDataArray = await ArtTileManager.getSceneSlideshowTiles(); let conflictingTile = tileDataArray.find((tileData) => { return tileData.displayName.includes(name); }); if (conflictingTile) { let digit = conflictingTile.displayName.match(/\d+/g); if (digit) { digit = parseInt(digit); digit++; } else { digit = 1; } finalName = finalName + digit; } return finalName; } static async createTileInScene(isFrameTile) { let ourScene = game.scenes.viewed; let pathProperty = isFrameTile ? "frameTilePath" : "artTilePath"; let imageManager = ImageDisplayManager; let imgPath = await HF.getSettingValue( "artGallerySettings", `defaultTileImages.paths.${pathProperty}` ); if (!imgPath) { return; } const tex = await loadTexture(imgPath); let sceneWidth = game.version >= 10 ? ourScene.width : ourScene.data.width; let sceneHeight = game.version >= 10 ? ourScene.height : ourScene.data.height; let dimensionObject = imageManager.calculateAspectRatioFit( tex.width, tex.height, sceneWidth, sceneHeight ); let newTile = await ourScene.createEmbeddedDocuments("Tile", [ { img: imgPath, width: dimensionObject.width, height: dimensionObject.height, x: sceneWidth / 2 - dimensionObject.width / 2, y: sceneHeight / 2 - dimensionObject.height / 2, }, ]); return newTile; } static async createOrFindDefaultFrameTile() { let frameTile; let tileDataArray = (await ArtTileManager.getSceneSlideshowTiles()) || []; if (tileDataArray.length === 0) { ui.notifications.warn( "No frame tile detected in scene. Creating new frame tile alongside display tile" ); frameTile = await ArtTileManager.createFrameTileObject(); return frameTile[0].id; } else { ui.notifications.warn( "No frame tile provided to when creating display tile. Linking display tile to first frame tile in scene" ); frameTile = tileDataArray[0]; return frameTile.id; } } /** * Return any tiles that have data, but their id doesn't match any tile in the scene * @param {Array} flaggedTiles - array of tiles with Slideshow data * @returns array of tiles that have data but aren't connected to a tile in the scene */ static async getMissingTiles(flaggedTiles) { let currentScene = game.scenes.viewed; let sceneTileIDs = currentScene.tiles.map((tile) => tile.id); let missingTiles = flaggedTiles.filter( (tileData) => !sceneTileIDs.includes(tileData.id) ); return missingTiles; } /** * Get tiles that aren't linked to any slideshow data * @param {Array} flaggedTiles - array of tiles with Slideshow data * @returns array of IDs of tiles that aren't linked to any slideshow data */ static async getUnlinkedTileIDs(flaggedTiles) { let currentScene = game.scenes.viewed; let flaggedTileIDs = flaggedTiles.map((tileData) => tileData.id); let sceneTileIDs = currentScene.tiles.map((tile) => tile.id); let unlinkedTiles = sceneTileIDs.filter( (tileID) => !flaggedTileIDs.includes(tileID) ); return unlinkedTiles; } /** * * @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one * @param {String} tileObjectID - the ID of the tile being created * @param {Boolean} isBoundingTile - whether or not its a frame tile instead of an art tile */ static async createTileData(linkedFrameTileId, tileObjectID, isBoundingTile = false) { let defaultData = await ArtTileManager.getDefaultData( isBoundingTile, linkedFrameTileId ); await ArtTileManager.updateSceneTileFlags(defaultData, tileObjectID); } /** * Create a Tile in the current scene that is linked to the Tile Data we're passing in * @param {Object} options - the options object * @param {Boolean} options.isFrameTile - whether or not its a frame tile or an art tile * @param {String} options.linkedFrameTileID - the ID of the linked frame tile * @param {String} options.unlinkedDataID - the ID of the unlinked art gallery tile we want to link to a tile in the scene * @returns the new tile object in the scene that is linked to our previously unlinked data */ static async createAndLinkSceneTile( options = { isFrameTile: false, linkedFrameTileID: "", unlinkedDataID: "", } ) { let { isFrameTile, linkedFrameTileID, unlinkedDataID } = options; let newTile = await ArtTileManager.createTileInScene(isFrameTile); if (newTile) { let tileObjectID = newTile[0].id; // if (!unlinkedDataID) { console.log("Scene Gallery Config - Creating new tile data"); await ArtTileManager.createTileData( linkedFrameTileID, tileObjectID, false ); } else { console.log( "Scene Gallery Config | updating already created tile data, and linking it" ); await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID); } } else { ui.notifications.error("New art gallery tile couldn't be created"); } return newTile; } /** * * @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one * @param {*} unlinkedDataID - if we're creating a new tile from the config rather than from the tile itself, it may have an unlinkedId * @returns - the created art tile */ static async createArtTileObject(_linkedFrameTileId = "", unlinkedDataID = "") { let linkedFrameTileId = _linkedFrameTileId; let newTile = await ArtTileManager.createTileInScene(false); if (newTile) { let tileObjectID = newTile[0].id; if (!unlinkedDataID) { await ArtTileManager.createTileData( linkedFrameTileId, tileObjectID, false ); } else { await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID); } } else { ui.notifications.error("New display tile couldn't be created"); } return newTile; } /** * @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one * @param {*} unlinkedDataID - if we're creating a new tile from the config rather than from the tile itself, it may have an unlinkedId * @returns - the created frame tile * */ static async createFrameTileObject(unlinkedDataID = "") { let newTile = await ArtTileManager.createTileInScene(true); if (newTile) { let tileObjectID = newTile[0].id; // let tileObjectID = newTile[0].document.id; if (!unlinkedDataID) { await ArtTileManager.createTileData("", tileObjectID, true); } else { await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID); } } else { ui.notifications.error("New frame tile couldn't be created"); } return newTile; } static async convertToNewSystem() { let currentScene = game.scenes.viewed; let sceneTiles = currentScene.tiles.contents; let boundingTile; let displayTile; sceneTiles.forEach(async (tile) => { let flag = await tile.getFlag("journal-to-canvas-slideshow", "name"); switch (flag.name) { case "boundingTile": boundingTile = tile; break; case "displayTile": displayTile = tile; break; default: break; } }); if (boundingTile) { convertBoundingTile(boundingTile.data); if (displayTile) { convertDisplayTile(displayTile.data, boundingTile.id); } } else { if (displayTile) { convertDisplayTile(displayTile.data); } } } static convertBoundingTile(tileData) { let defaultData = { displayName: "BoundingTile1", isBoundingTile: true, linkedBoundingTile: "", }; updateSceneTileFlags(defaultData, tileData.id); } static convertDisplayTile(tileData, linkedBoundingTileId = "") { let defaultData = { displayName: "DisplayTile1", isBoundingTile: false, linkedBoundingTile: linkedBoundingTileId, }; updateSceneTileFlags(defaultData, tileData.id); } /** * Set the default art gallery art tile in this scene * @param {String} tileID - the id of the art gallery tile that was clicked * @param {Object} currentScene - the current scene doc */ static async setDefaultArtTileID(tileID, currentScene) { if (!currentScene) currentScene = game.scenes.viewed; await currentScene.setFlag(MODULE_ID, "defaultArtTileID", tileID); Hooks.callAll("updateDefaultArtTile", { updateData: tileID, currentScene }); } /** * Gets the default art gallery tile in this scene, or returns the first tile in scene if not found * returns undefined if that is also not found * @param {Object} currentScene - the current scene doc */ static async getDefaultArtTileID(currentScene) { if (!currentScene) currentScene = game.scenes.viewed; //get all the art tiles in the scene, filtering out the ones that aren't unlinked/missing let artTiles = ( await ArtTileManager.getSceneSlideshowTiles("art", true, { currentSceneID: currentScene.id, }) ).filter((item) => !item.missing); let defaultArtTileID = await currentScene.getFlag(MODULE_ID, "defaultArtTileID"); // if the defaultArtTileID stored in our flags is let shouldReplaceID = false; const found = artTiles.find((tileData) => tileData.id === defaultArtTileID); if (!found) { //a tile with this ID wasn't in the scene, so replace it with a the first art tile that IS linked, or if there are none, replace it with an empty string shouldReplaceID = true; if (artTiles.length > 0) { defaultArtTileID = artTiles[0].id; } else { defaultArtTileID = ""; } } // should if (shouldReplaceID) { await currentScene.setFlag(MODULE_ID, "defaultArtTileID", defaultArtTileID); } return defaultArtTileID; } static async getGalleryTileDataFromID(tileID, property = "", currentSceneID = "") { if (!currentSceneID) currentSceneID = game.scenes.viewed.current; let flaggedTiles = await ArtTileManager.getSceneSlideshowTiles("", false, { currentSceneID, }); let ourTile = flaggedTiles.find((data) => data.id === tileID); if (property) { if (ourTile) { return ourTile[property]; } else { return ""; } } else { return ourTile; } } /** * get tiles that have been stored by this module in a flag on this scene * @returns array of display tile data stored in "slideshowTiles" tag */ static async getSceneSlideshowTiles( type = "", shouldCheckIfExists = false, options = { currentSceneID: "" } ) { let { currentSceneID } = options; let currentScene = game.scenes.viewed; if (currentSceneID) { currentScene = game.scenes.get(currentSceneID); } let flaggedTiles = (await currentScene.getFlag( "journal-to-canvas-slideshow", "slideshowTiles" )) || []; //check if the tile exists in the scene, and add a "missing" element if it does if (shouldCheckIfExists) { //get the ids of all the tiles in the scene let sceneTileIDs = currentScene.tiles.map((tile) => tile.id); // if our tile data's id isn't included in the ids of tiles in the scene, add a missing property flaggedTiles = flaggedTiles.map((tileData) => !sceneTileIDs.includes(tileData.id) ? { ...tileData, missing: true } : tileData ); } if (type === "frame") { return ArtTileManager.getFrameTiles(flaggedTiles); } else if (type === "art") { return ArtTileManager.getDisplayTiles(flaggedTiles); } return flaggedTiles; } /** * Returns the "bounding" or "frame" tiles from the scene's displayTiles flag * @param {Array} flaggedTiles - array of Display Tiles from a scene flag * @returns filtered array with only the bounding tiles */ static getFrameTiles(flaggedTiles) { return flaggedTiles.filter((tileData) => tileData.isBoundingTile); } /** * Returns the Display tiles from the scene's displayTiles flag array * @param {Array} flaggedTiles - array of SlideshowTiles from the scene's flag * @returns filtered array with only the display tiles */ static getDisplayTiles(flaggedTiles) { return flaggedTiles.filter((tileData) => !tileData.isBoundingTile); } static async renderTileConfig(tileID, sceneID = "") { let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID); if (tile) { await tile.sheet.render(true); } else { ArtTileManager.displayTileNotFoundError(tileID); } } static async toggleTileZ(tileID, toFront = true) { let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID); if (!tile) return; const zIndex = tile.object.zIndex; if (toFront) { //save the default z-index tile.defaultZ = zIndex; await game.scenes.viewed.updateEmbeddedDocuments("Tile", { _id: tileID, object: { zIndex: 300 }, }); } else { await game.scenes.viewed.updateEmbeddedDocuments("Tile", { _id: tileID, object: { zIndex: tile.defaultZ }, }); } } static async selectTile(tileID, sceneID = "") { let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID); if (tile) { await game.JTCS.utils.swapTools(); tile.object.control({ releaseOthers: true, }); } else { ArtTileManager.displayTileNotFoundError(tileID); } } static async getLinkedFrameID(tileID, flaggedTiles) { let tileData = await ArtTileManager.getTileDataFromFlag(tileID, flaggedTiles); if (!tileData) { console.error("Could not find tile data with that ID"); } return tileData.linkedBoundingTile; } /** * Get the DisplayTile data * @param {string} tileID - the id of the tile in scene we're looking to filter * @param {Array} flaggedTiles - the flagged tiles * @returns the flag data */ static async getTileDataFromFlag(tileID, flaggedTiles) { let defaultData = ArtTileManager.getDefaultData(false, ""); if (!flaggedTiles) { return defaultData; } let flaggedTile = flaggedTiles.find((tileData) => tileData.id === tileID); //if we find a tile if (flaggedTile) { return flaggedTile; } else { return defaultData; } } static async getTileObjectByID(tileID, sceneID = "") { let ourScene = game.scenes.viewed; if (sceneID) ourScene = game.scenes.get(sceneID); let tile = await ourScene.getEmbeddedDocument("Tile", tileID); if (tileID.includes === "new") { // console.log("New tile created"); } else { if (!tile) { ArtTileManager.displayTileNotFoundError(tileID); } } return tile; } static displayTileNotFoundError(tileID, sceneID = "") { console.error("JTCS can't find tile in scene with ID " + tileID); } /** * Update a tile in this scene with new data, or create a new one * @param {Object} displayData - the data we want to update with * @param {String} tileID - the id of the tile we want to update */ static async updateSceneTileFlags(displayData, tileID) { if (!tileID) { return; } let currentScene = game.scenes.viewed; let tiles = (await ArtTileManager.getSceneSlideshowTiles()) || []; if (tiles.find((tile) => tile.id === tileID)) { tiles = tiles.map((tileData) => { // if the ids match, update the matching one with the new displayName return tileData.id === tileID ? { ...tileData, ...displayData } : tileData; //else just return the original }); } else { tiles.push({ id: tileID, ...displayData }); } await ArtTileManager.updateAllSceneTileFlags(tiles); } /** * Replace the slideshow tileData array stored in scene flags with the array passed in * @param {Array} tiles - the tiles array we want to update our flag with */ static async updateAllSceneTileFlags(tiles, currentSceneID = "") { let currentScene = game.scenes.get(currentSceneID); if (!currentScene) currentScene = game.scenes.viewed; await currentScene.setFlag( "journal-to-canvas-slideshow", "slideshowTiles", tiles ); Hooks.callAll("updateArtGalleryTiles", { currentScene, updateData: tiles }); } static async deleteSceneTileData(tileID) { let tiles = await ArtTileManager.getSceneSlideshowTiles(); //filter out the tile that matches this let deletedTileData = tiles.find((tileData) => tileData.id === tileID); tiles = tiles.filter((tileData) => tileData.id !== tileID); if (deletedTileData.isBoundingTile) { await ArtTileManager.updateLinkedArtTiles(tileID, "", tiles); } await ArtTileManager.updateAllSceneTileFlags(tiles); //call hook to delete the art tile data } static async getAllScenesWithSlideshowData() { let slideshowScenes = game.scenes.contents.filter((scene) => { if (game.version >= 10) return scene.flags[`${MODULE_ID}`]?.slideshowTiles; else return scene.data.flags[`${MODULE_ID}`]?.slideshowTiles; }) || []; return slideshowScenes; } }