Add files via upload

This commit is contained in:
2025-08-17 15:03:39 +02:00
committed by GitHub
commit 7d6e5fab53
6 changed files with 418 additions and 0 deletions

59
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
}