From 7d6e5fab535a9ae66924ec3078921b92eaf20509 Mon Sep 17 00:00:00 2001 From: SocksOnHead Date: Sun, 17 Aug 2025 15:03:39 +0200 Subject: [PATCH] Add files via upload --- README.md | 59 +++++++++ languages/en.json | 16 +++ module.json | 28 ++++ scripts/easy-battlemap.js | 14 ++ scripts/lib/drop-handler.js | 253 ++++++++++++++++++++++++++++++++++++ styles/easy-battlemap.css | 48 +++++++ 6 files changed, 418 insertions(+) create mode 100644 README.md create mode 100644 languages/en.json create mode 100644 module.json create mode 100644 scripts/easy-battlemap.js create mode 100644 scripts/lib/drop-handler.js create mode 100644 styles/easy-battlemap.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2afa66 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Easy Battlemap for Foundry VTT + +This module allows you to quickly create battlemaps in Foundry VTT by simply dragging and dropping a background image and a JSON file containing wall data. + +## Installation + +1. In the Foundry VTT setup screen, go to "Add-on Modules" tab +2. Click "Install Module" +3. Paste the following URL in the "Manifest URL" field: + `https://github.com/MyxeliumI/easy-battlemap/releases/latest/download/module.json` +4. Click "Install" + +## Usage + +1. Enable the module in your game world +2. Navigate to the "Scenes" tab +3. You'll see a new "Easy Battlemap Creator" panel +4. Drag and drop your background image (jpg, png) +5. Drag and drop your JSON file with wall data +6. Once both files are loaded, click "Create Battlemap Scene" + +## JSON Format + +The JSON file should contain wall data in the following format: + +```json +{ + "walls": [ + { + "c": [x1, y1, x2, y2], + "door": 0, + "move": 0, + "sense": 0, + "dir": 0, + "ds": 0, + "flags": {} + }, + // ... more walls + ], + "lights": [ + // optional light data + ], + "notes": [ + // optional note data + ], + "tokens": [ + // optional token data + ], + "drawings": [ + // optional drawing data + ] +} +``` + +For the specific format details, refer to the [Foundry VTT REST API Relay documentation](https://github.com/ThreeHats/foundryvtt-rest-api-relay/wiki/create-POST#request-payload). + +## License + +This module is licensed under the MIT License. \ No newline at end of file diff --git a/languages/en.json b/languages/en.json new file mode 100644 index 0000000..a7db2db --- /dev/null +++ b/languages/en.json @@ -0,0 +1,16 @@ +{ + "EASYBATTLEMAP": { + "Ready": "Easy Battlemap module is ready", + "DropAreaTitle": "Easy Battlemap Creator", + "DropInstructions": "Drop a background image and a JSON file with wall data here", + "BackgroundStatus": "Background Image", + "WallDataStatus": "Wall Data", + "CreateScene": "Create Battlemap Scene", + "MissingFiles": "Both background image and wall data JSON are required", + "CreatingScene": "Creating battlemap scene...", + "SceneCreated": "Battlemap scene created", + "UploadFailed": "Failed to upload background image", + "SceneCreationFailed": "Failed to create scene", + "InvalidJSON": "The JSON file could not be parsed" + } +} \ No newline at end of file diff --git a/module.json b/module.json new file mode 100644 index 0000000..ae7c087 --- /dev/null +++ b/module.json @@ -0,0 +1,28 @@ +{ + "id": "easy-battlemap", + "title": "Easy Battlemap", + "description": "Create battlemaps by simply dragging in a background image and wall data JSON file", + "version": "1.0.0", + "compatibility": { + "minimum": "10", + "verified": "11" + }, + "authors": [ + { + "name": "MyxeliumI", + "url": "https://github.com/MyxeliumI" + } + ], + "esmodules": ["scripts/easy-battlemap.js"], + "styles": ["styles/easy-battlemap.css"], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "languages/en.json" + } + ], + "url": "https://github.com/MyxeliumI/easy-battlemap", + "manifest": "https://github.com/MyxeliumI/easy-battlemap/releases/latest/download/module.json", + "download": "https://github.com/MyxeliumI/easy-battlemap/releases/latest/download/easy-battlemap.zip" +} \ No newline at end of file diff --git a/scripts/easy-battlemap.js b/scripts/easy-battlemap.js new file mode 100644 index 0000000..bcf1587 --- /dev/null +++ b/scripts/easy-battlemap.js @@ -0,0 +1,14 @@ +import { EasyBattlemapDropHandler } from './lib/drop-handler.js'; + +Hooks.once('init', async function() { + console.log('Easy Battlemap | Initializing Easy Battlemap'); +}); + +Hooks.once('ready', async function() { + console.log('Easy Battlemap | Ready'); + + // Register the drop handler when Foundry is ready + new EasyBattlemapDropHandler().registerDropHandler(); + + ui.notifications.info(game.i18n.localize("EASYBATTLEMAP.Ready")); +}); \ No newline at end of file diff --git a/scripts/lib/drop-handler.js b/scripts/lib/drop-handler.js new file mode 100644 index 0000000..195d433 --- /dev/null +++ b/scripts/lib/drop-handler.js @@ -0,0 +1,253 @@ +export class EasyBattlemapDropHandler { + constructor() { + this.backgroundImage = null; + this.wallData = null; + this.processingQueue = {}; + } + + registerDropHandler() { + // Add the drop area to the sidebar + this._addDropArea(); + + // Register the drop handler on the canvas + this._registerCanvasDropHandler(); + + // Register the drop handler for the custom drop area + this._registerDropAreaHandler(); + } + + _addDropArea() { + const dropAreaHTML = ` +
+
+

${game.i18n.localize("EASYBATTLEMAP.DropAreaTitle")}

+
+
+
+ ${game.i18n.localize("EASYBATTLEMAP.DropInstructions")} +
+
+
+ ${game.i18n.localize("EASYBATTLEMAP.BackgroundStatus")}: +
+
+ ${game.i18n.localize("EASYBATTLEMAP.WallDataStatus")}: +
+
+ +
+
+ `; + + // Add to sidebar + const sidebarTab = document.getElementById('scenes'); + if (sidebarTab) { + sidebarTab.insertAdjacentHTML('afterend', dropAreaHTML); + + // Add click handler for the create button + document.querySelector('.create-scene-button').addEventListener('click', () => { + this._createScene(); + }); + } + } + + _registerCanvasDropHandler() { + // Handle drops on the canvas + canvas.stage.on('drop', (event) => { + this._handleDrop(event.data.originalEvent); + }); + } + + _registerDropAreaHandler() { + const dropArea = document.getElementById('easy-battlemap-drop-area'); + if (!dropArea) return; + + // Add event listeners for drag and drop + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, (e) => { + e.preventDefault(); + e.stopPropagation(); + }, false); + }); + + // Handle drag enter/over + ['dragenter', 'dragover'].forEach(eventName => { + dropArea.addEventListener(eventName, () => { + dropArea.classList.add('highlight'); + }, false); + }); + + // Handle drag leave/drop + ['dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, () => { + dropArea.classList.remove('highlight'); + }, false); + }); + + // Handle drop + dropArea.addEventListener('drop', (e) => { + this._handleDrop(e); + }, false); + } + + _handleDrop(event) { + const files = event.dataTransfer.files; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + if (file.type.match('image.*')) { + this._processImageFile(file); + } else if (file.name.endsWith('.json')) { + this._processJSONFile(file); + } + } + } + + _processImageFile(file) { + const reader = new FileReader(); + + reader.onload = (e) => { + this.backgroundImage = { + data: e.target.result, + filename: file.name + }; + + // Update the UI to show the background is ready + document.querySelector('.background-status .status-value').textContent = '✅'; + document.querySelector('.background-status .status-value').title = file.name; + + this._checkReadyToCreate(); + }; + + reader.readAsDataURL(file); + } + + _processJSONFile(file) { + const reader = new FileReader(); + + reader.onload = (e) => { + try { + const jsonData = JSON.parse(e.target.result); + this.wallData = jsonData; + + // Update the UI to show the wall data is ready + document.querySelector('.wall-data-status .status-value').textContent = '✅'; + document.querySelector('.wall-data-status .status-value').title = file.name; + + this._checkReadyToCreate(); + } catch (error) { + ui.notifications.error(game.i18n.localize("EASYBATTLEMAP.InvalidJSON")); + } + }; + + reader.readAsText(file); + } + + _checkReadyToCreate() { + const createButton = document.querySelector('.create-scene-button'); + if (this.backgroundImage && this.wallData) { + createButton.disabled = false; + } else { + createButton.disabled = true; + } + } + + async _createScene() { + if (!this.backgroundImage || !this.wallData) { + ui.notifications.error(game.i18n.localize("EASYBATTLEMAP.MissingFiles")); + return; + } + + try { + ui.notifications.info(game.i18n.localize("EASYBATTLEMAP.CreatingScene")); + + // First, we need to upload the background image to the server + const uploadResponse = await this._uploadImage(this.backgroundImage); + + if (!uploadResponse.path) { + ui.notifications.error(game.i18n.localize("EASYBATTLEMAP.UploadFailed")); + return; + } + + // Extract scene name from the image filename + const sceneName = this.backgroundImage.filename.split('.').slice(0, -1).join('.'); + + // Get image dimensions + const img = new Image(); + img.src = this.backgroundImage.data; + + await new Promise(resolve => { + img.onload = resolve; + }); + + // Create the scene + const sceneData = { + name: sceneName, + img: uploadResponse.path, + width: img.width, + height: img.height, + padding: 0, + backgroundColor: "#000000", + grid: { + type: 1, // Square grid + size: 100, + color: "#000000", + alpha: 0.2 + }, + walls: this.wallData.walls || [], + lights: this.wallData.lights || [], + notes: this.wallData.notes || [], + tokens: this.wallData.tokens || [], + drawings: this.wallData.drawings || [] + }; + + const scene = await Scene.create(sceneData); + + // Reset the data after creating the scene + this.backgroundImage = null; + this.wallData = null; + + // Reset the UI + document.querySelector('.background-status .status-value').textContent = '❌'; + document.querySelector('.wall-data-status .status-value').textContent = '❌'; + document.querySelector('.create-scene-button').disabled = true; + + ui.notifications.success(`${game.i18n.localize("EASYBATTLEMAP.SceneCreated")}: ${sceneName}`); + + // Activate the scene + await scene.activate(); + + } catch (error) { + console.error(error); + ui.notifications.error(game.i18n.localize("EASYBATTLEMAP.SceneCreationFailed")); + } + } + + async _uploadImage(imageData) { + const formData = new FormData(); + + // Convert base64 to a blob + const base64Response = await fetch(imageData.data); + const blob = await base64Response.blob(); + + formData.append('file', blob, imageData.filename); + formData.append('path', 'scenes'); + + const response = await fetch('/upload', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${game.data.session}` + }, + body: formData + }); + + if (!response.ok) { + throw new Error('Upload failed'); + } + + return await response.json(); + } +} \ No newline at end of file diff --git a/styles/easy-battlemap.css b/styles/easy-battlemap.css new file mode 100644 index 0000000..6668030 --- /dev/null +++ b/styles/easy-battlemap.css @@ -0,0 +1,48 @@ +#easy-battlemap-drop-area { + margin-bottom: 10px; + background: rgba(0, 0, 0, 0.1); + border: 1px solid #7a7971; + border-radius: 5px; +} + +#easy-battlemap-drop-area header { + background: rgba(0, 0, 0, 0.2); + padding: 5px; + border-bottom: 1px solid #7a7971; + text-align: center; +} + +.easy-battlemap-dropzone { + padding: 15px; + text-align: center; + min-height: 120px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.easy-battlemap-instructions { + font-style: italic; + color: #777; + margin-bottom: 10px; +} + +.easy-battlemap-status { + display: flex; + justify-content: space-around; + margin: 10px 0; +} + +.easy-battlemap-status .status-value { + font-weight: bold; +} + +.create-scene-button { + width: 80%; + margin: 0 auto; +} + +#easy-battlemap-drop-area.highlight { + border-color: #ff6400; + background-color: rgba(255, 100, 0, 0.1); +} \ No newline at end of file