Add files via upload
This commit is contained in:
59
README.md
Normal file
59
README.md
Normal file
@@ -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.
|
||||
16
languages/en.json
Normal file
16
languages/en.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
28
module.json
Normal file
28
module.json
Normal file
@@ -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"
|
||||
}
|
||||
14
scripts/easy-battlemap.js
Normal file
14
scripts/easy-battlemap.js
Normal file
@@ -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"));
|
||||
});
|
||||
253
scripts/lib/drop-handler.js
Normal file
253
scripts/lib/drop-handler.js
Normal file
@@ -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 = `
|
||||
<div id="easy-battlemap-drop-area" class="app">
|
||||
<header class="app-header flexrow">
|
||||
<h4>${game.i18n.localize("EASYBATTLEMAP.DropAreaTitle")}</h4>
|
||||
</header>
|
||||
<section class="easy-battlemap-dropzone">
|
||||
<div class="easy-battlemap-instructions">
|
||||
${game.i18n.localize("EASYBATTLEMAP.DropInstructions")}
|
||||
</div>
|
||||
<div class="easy-battlemap-status">
|
||||
<div class="background-status">
|
||||
${game.i18n.localize("EASYBATTLEMAP.BackgroundStatus")}: <span class="status-value">❌</span>
|
||||
</div>
|
||||
<div class="wall-data-status">
|
||||
${game.i18n.localize("EASYBATTLEMAP.WallDataStatus")}: <span class="status-value">❌</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="create-scene-button" disabled>
|
||||
${game.i18n.localize("EASYBATTLEMAP.CreateScene")}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
48
styles/easy-battlemap.css
Normal file
48
styles/easy-battlemap.css
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user