Your Google Apps Script backend is outdated. This version of the app requires new database columns to prevent data loss.
Enter your PIN to manage your inventory.
Enter your PIN to switch to Dungeon Master tools.
Coins calculated at 50 per lb. Combat weight includes Equipped, Belt, and Bandolier items.
To save data, this app connects to a Google Sheet using Google Apps Script.
const SHEET_NAME = "LootData";
const PLAYER_SHEET = "Players";
const FOLDER_NAME = "Party_Loot_Images";
const BACKEND_VERSION = "2.0";
function getOrCreateFolder() {
const f = DriveApp.getFoldersByName(FOLDER_NAME);
return f.hasNext() ? f.next() : DriveApp.createFolder(FOLDER_NAME);
}
function doGet(e) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) throw new Error("Could not connect to the Google Sheet.");
let s = ss.getSheetByName(SHEET_NAME);
let p = ss.getSheetByName(PLAYER_SHEET) || ss.getSheetByName("players") || ss.getSheetByName("Players ");
let items = [];
if (s) {
let data = s.getDataRange().getValues();
if (data.length === 1 && typeof data[0][0] === 'string' && data[0][0].trim().startsWith('[')) {
items = JSON.parse(data[0][0]);
} else if (data.length > 1) {
let headers = data[0];
for (let i = 1; i < data.length; i++) {
let item = {};
headers.forEach((h, index) => { item[h] = data[i][index]; });
items.push(item);
}
}
}
let players = [];
if (p) {
let data = p.getDataRange().getValues();
for(let i = 0; i < data.length; i++) {
let name = String(data[i][0] || "").trim();
let pin = String(data[i][1] || "").trim();
let character = String(data[i][2] || "").trim();
if (!name || !pin) continue;
if (name.toLowerCase() === "player name" || name.toLowerCase() === "name") continue;
players.push({
name: name,
pin: pin,
character: character
});
}
} else {
throw new Error("Could not find the 'Players' tab.");
}
return ContentService.createTextOutput(JSON.stringify({backendVersion: BACKEND_VERSION, items: items, players: players})).setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({backendVersion: BACKEND_VERSION, items: [], players: [], error: err.toString()})).setMimeType(ContentService.MimeType.JSON);
}
}
function doPost(e) {
try {
const p = JSON.parse(e.postData.contents);
if (p.action === 'uploadImage') {
const f = getOrCreateFolder(), b = Utilities.newBlob(Utilities.base64Decode(p.base64), p.mimeType, p.fileName);
const file = f.createFile(b); file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return ContentService.createTextOutput(JSON.stringify({ url: "https://drive.google.com/uc?id=" + file.getId() })).setMimeType(ContentService.MimeType.JSON);
}
if (p.action === 'saveData') {
let s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
if (!s) s = SpreadsheetApp.getActiveSpreadsheet().insertSheet(SHEET_NAME);
s.clear();
if (p.data && p.data.length > 0) {
let headers = ["id", "cardType", "apparentName", "trueName", "imageUrl", "owner", "storageLocation", "weight", "quantity", "maxQuantity", "hasQuantity", "isConsumable", "isEquipped", "requiresAttunement", "isAttuned", "isIdentified", "description", "isCurseRevealed", "curseDetectDC", "curseDetails", "secretKeeper", "cardSize", "rarity", "wealthData", "isCollapsed"];
let rows = [headers];
p.data.forEach(item => {
let rowData = headers.map(h => {
let val = item[h];
if (val === undefined || val === null) return "";
if (typeof val === 'boolean') return val ? "TRUE" : "FALSE";
if (typeof val === 'object') return JSON.stringify(val);
return val;
});
rows.push(rowData);
});
s.getRange(1, 1, rows.length, headers.length).setValues(rows);
}
return ContentService.createTextOutput(JSON.stringify({ status: "success" })).setMimeType(ContentService.MimeType.JSON);
}
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ error: err.toString() })).setMimeType(ContentService.MimeType.JSON);
}
}
Have players scan this QR code or use the link below to join!