Hello, my wife plays dnd on roll 20. I am working with Claude Code (I honestly have no idea what I am doing besides following directions). I am trying to get a site made for her to keep track of things all in one place because we are both scatter-brained. She wants to be able to contain session logs/character sheets and some other stuffs all on the site I am working on. One of the things we were trying to do is have character sheets linked to her roll 20 campaign so changes can be made from the tracker website and get pushed to roll20/ vice versa. Is this possible? Been struggling with it the last 2 days. Not sure if I am feeding direction into claude wrong or if it is just not possible. Below is the script Claude came up with. Any advice appreciated thanks. var CHARACTER_MAP = {
};
var TRACKED_ATTRS = {
'hp':true,'hp_max':true,'hp_temp':true,'ac':true,'speed':true,
'initiative':true,'hit_dice':true,'hit_dice_total':true,
'strength':true,'dexterity':true,'constitution':true,
'intelligence':true,'wisdom':true,'charisma':true,
'level':true,'xp':true,
'spell_slots_l1':true,'spell_slots_l2':true,'spell_slots_l3':true,
'spell_slots_l4':true,'spell_slots_l5':true,'spell_slots_l6':true,
'spell_slots_l7':true,'spell_slots_l8':true,'spell_slots_l9':true,
'spell_slots_total_l1':true,'spell_slots_total_l2':true,'spell_slots_total_l3':true,
'spell_slots_total_l4':true,'spell_slots_total_l5':true,'spell_slots_total_l6':true,
'spell_slots_total_l7':true,'spell_slots_total_l8':true,'spell_slots_total_l9':true
};
var pendingOut = {};
function httpPost(path, data, onDone) {
fetch(SERVER_URL + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + SYNC_TOKEN
},
body: JSON.stringify(data)
}).then(function(res) {
return res.json();
}).then(function(json) {
if (onDone) onDone(json);
}).catch(function(e) {
log('[Sync] POST error: ' + e.message);
});
}
function httpGet(path, onDone) {
fetch(SERVER_URL + path, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + SYNC_TOKEN
}
}).then(function(res) {
return res.json();
}).then(function(json) {
onDone(json);
}).catch(function(e) {
log('[Sync] GET error: ' + e.message);
});
}
function flushPending(trackerCharId, roll20CharId) {
var buf = pendingOut[trackerCharId];
if (!buf) return;
var fields = {};
var hasFields = false;
for (var k in buf) { if (k !== '_timer') { fields[k] = buf[k]; hasFields = true; } }
if (!hasFields) return;
pendingOut[trackerCharId] = {};
httpPost('/api/roll20/push', {characterId: trackerCharId, roll20CharId: roll20CharId, fields: fields}, function(result) {
log('[Sync] Pushed ' + result.applied + ' field(s) for ' + trackerCharId + (result.conflicts > 0 ? ' (' + result.conflicts + ' conflict(s))' : ''));
});
}
function bufferChange(trackerCharId, roll20CharId, attrName, val) {
if (!pendingOut[trackerCharId]) pendingOut[trackerCharId] = {};
pendingOut[trackerCharId][attrName] = val;
clearTimeout(pendingOut[trackerCharId]._timer);
pendingOut[trackerCharId]._timer = setTimeout(function() { flushPending(trackerCharId, roll20CharId); }, 3000);
}
function applyPending(roll20CharId, items) {
if (!items || !items.length) return;
var applied = 0;
for (var i = 0; i < items.length; i++) {
var fields = items[i].fields;
if (!fields) continue;
for (var attrName in fields) {
var val = fields[attrName];
var attrs = findObjs({_type: 'attribute', characterid: roll20CharId, name: attrName});
if (attrs.length > 0) { attrs[0].set({current: val}); }
else { createObj('attribute', {characterid: roll20CharId, name: attrName, current: val}); }
applied++;
}
}
if (applied > 0) { log('[Sync] Applied ' + applied + ' update(s) to ' + roll20CharId); }
}
function pollPending() {
for (var roll20CharId in CHARACTER_MAP) {
(function(r20Id) {
var trackerCharId = CHARACTER_MAP[r20Id];
httpGet('/api/roll20/pending?characterId=' + encodeURIComponent(trackerCharId), function(items) { applyPending(r20Id, items); });
}(roll20CharId));
}
}
on('change:attribute', function(obj) {
var roll20CharId = obj.get('characterid');
var trackerCharId = CHARACTER_MAP[roll20CharId];
if (!trackerCharId) return;
var attrName = obj.get('name');
if (!TRACKED_ATTRS[attrName]) return;
var val = parseFloat(obj.get('current'));
if (isNaN(val)) return;
bufferChange(trackerCharId, roll20CharId, attrName, val);
});
on('chat:message', function(msg) {
if (msg.type !== 'api') return;
var trimmed = msg.content.trim();
var spaceIdx = trimmed.indexOf(' ');
var cmd = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase();
if (cmd === '!sync-status') {
var linked = Object.keys(CHARACTER_MAP);
var lines = ['<b>Sync</b> Server: ' + SERVER_URL, 'Linked: ' + linked.length];
for (var i = 0; i < linked.length; i++) { lines.push(linked[i] + ' -> ' + CHARACTER_MAP[linked[i]]); }
sendChat('Sync', lines.join('<br>'));
} else if (cmd === '!sync-push') {
var count = 0;
for (var r20Id in CHARACTER_MAP) { flushPending(CHARACTER_MAP[r20Id], r20Id); count++; }
sendChat('Sync', 'Pushed for ' + count + ' character(s).');
} else if (cmd === '!sync-pull') {
pollPending();
sendChat('Sync', 'Polling...');
}
});
on('ready', function() {
log('[Sync] Ready. Server: ' + SERVER_URL + ' | Characters: ' + Object.keys(CHARACTER_MAP).length);
if (Object.keys(CHARACTER_MAP).length === 0) { log('[Sync] WARNING: CHARACTER_MAP is empty.'); }
function schedulePoll() {
setTimeout(function() {
pollPending();
schedulePoll();
}, 5000);
}
schedulePoll();
});