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