Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

ParseSWStatblock Is Broken again :(

ParseSWStatblock   has stopped working with the new sheets for savage worlds.  no ones answer so far on the issue section for over a month.  can anyone fix this?
1558885885
The Aaron
Roll20 Production Team
API Scripter
Are you getting any sort of error?
1558891784
Kavini
Roll20 Production Team
Marketplace Creator
Sheet Author
Compendium Curator
I've been doing some testing in conjunction with Finderski's SW tabbed sheet group. It's still a very early draft, but this should work in most cases.IF you have issues, let me know and I'll make changes. // Unofficial Savage Worlds Statblock Importer // by Nic B. 16/05/19 // based on work by Jason.P & Pelwer // // INSTRUCTIONS // 1. Find yourself a SW stat-block // 2. Copy the stat block from *Name* on down // 3. Paste the stat block into the GM Notes Section of a token in your roll20 campaign. // (if you're having difficulties, use CTRL+SHIFT+V on Windows or Option+Shift+Command+V on Mac to paste *without* formatting) // 4. Make sure the creature's name is on its own line, and is prefixed with DD or [WC] if it is a wild card. // 5. Select the token // 6. In the chat box, type the command "!SW-Import". // var SWImporter = SWImporter || ( function () { const vocal = 1; const elementTypes = { 'Attributes' : { delimiters: [',',' '], }, 'Skills' : { name: 'Skills', delimiters: [',',' '], }, 'Pace' : {}, 'Parry' : {}, 'Toughness' : {}, 'Edges' : { delimiters: [','], repeating: 'edge', }, 'Hindrances' : { delimiters: [','], repeating: 'hindrance', }, 'Special Abilities' : { delimiters: ['~',':'], repeating: 'attribute', }, 'Powers' : { delimiters: [','], repeating: 'power', }, 'PowerPoints' : {}, 'Gear' : {}, }; const wildcardPrefixes = [ "DD", "[WC]", ]; const entityMap = { '\<span .*?\>':'', '\<\/span\>':'', '\<p\>':'', '\<\/p\>':'', '\<br\>':'', '\ \;':'~', ' ':'~', ';':'', '-':'', '−':'-', '′′|“':'\"', '′':'\'', '\\s+':' ', '[^\x00-\x7F]+':'~', 'Common Knowledge':'CommonKnowledge', 'Weird Science':'WeirdScience', 'Native Language':'Language', 'Academics':'AcademicsSkill', 'Power Points':'PowerPoints', 'T\\s*o\\s*u\\s*g\\s*h\\s*n\\s*e\\s*s\\s*s\\s*':'Toughness', 'Language \\(\(\\w+\) \(.*?\)\\)':'Language($1) $2', }; let character = {}; const handleInput = (message) => { const messageType = message.type; const messageArguments = message.content.split(" --"); const selectedObject = retrieveSelectedObject(message.selected); if (messageType !== "api") return; if (messageArguments[0] !== "!SW-Import") return; if (messageArguments[1] === "help") { showHelp(); return; } if (!selectedObject) { sendChat("ERROR", "No Token Selected."); return; } if (selectedObject.get('subtype') !== 'token') { sendChat("ERROR","Must select a token, not a drawing or a card."); return; } const inputNotes = selectedObject.get('gmnotes'); importCharacter(inputNotes); }; const retrieveSelectedObject = (selected) => { if (selected && selected.length > 0) { return getObj('graphic', selected[0]._id); } else { return false; } }; const importCharacter = (inputNotes) => { const inputSanitized = sanitizeInput(inputNotes); const inputArray = identityKeywords(inputSanitized).split("@"); parseElements(inputArray); createCharacter(); }; const sanitizeInput = (inputNotes) => { const mapFindReplace = Object.entries(entityMap); let inputSanitized = unescape(inputNotes).replace(/\<\/p\>/,'@'); // cleans up the percent coding, adds an @ to the end of the name. mapFindReplace.forEach(([find, replace]) => { const regularExpression = new RegExp(find,"g"); inputSanitized = inputSanitized.replace(regularExpression,replace); }); return inputSanitized; }; const identityKeywords = (inputSanitized) => { const entityList = Object.keys(elementTypes).map(x => `(${x}:)`); // prepares const elementTypes for regular expression. i.e. Attributes => (Attributes:) let inputKeyworded = inputSanitized; entityList.forEach( (entity) => { regularExpression = new RegExp(entity,"g"); inputKeyworded = inputKeyworded.replace(regularExpression,'@$1'); }); return inputKeyworded; }; const parseElements = (inputArray) => { const firstLine = inputArray.shift(); character.wildCard = identifyWildCardPrefix(firstLine); character.name = firstLine.substring(character.wildCard.length).trim(); inputArray.forEach( (statblockElement) => { const elementType = getElementType(statblockElement); if(elementType) { const delimiters = (elementTypes[elementType].delimiters !== undefined) ? elementTypes[elementType].delimiters : undefined; if(elementType.length === 0) { character.description += statblockElement; return; } statblockElement = (elementType && statblockElement !== 'undefined') ? statblockElement.substring(elementType.length+1).trim() : statblockElement; character[elementType] = explodeElementToArray(statblockElement,delimiters); } }); }; const identifyWildCardPrefix = (firstLine) => { let wildCard = ""; wildcardPrefixes.forEach((prefix) => { wildCard = (firstLine.indexOf(prefix) !== -1) ? prefix : wildCard; }); return wildCard; }; const getElementType = (arrayItem) => { let elementType = ""; Object.keys(elementTypes).some((type) => { elementType = (arrayItem.indexOf(`${type}: `) !== -1) ? type : elementType; }); return elementType; }; const getDelimiters = (elementType) => { let elementDelimiters = []; Object.entries(elementTypes).forEach(([key,values]) => { elementDelimiters = (key === elementType) ? values.delimiters : elementDelimiters; }); if (elementDelimiters !== undefined) {return elementDelimiters;} return sendChat("ERROR",`Invalid element type. ${elementType}`); } const explodeElementToArray = (input, delimiters) => { if(!delimiters || delimiters.length === 0) return input; const output = input.trim() .split(delimiters[0]) .map(x => explodeElementToArray(x, delimiters.slice(1))); return output; }; const convertDiceToNumbers = (value) => { return value.replace(/d(.*)$/,"$1"); } const createCharacter = () => { if (characterSheetExists(character.name)) { sendChat("ERROR","This character already exists."); return; } characterObject = initializeCharacterSheet(character.name); character.id = characterObject.get('_id'); if(character.wildCard) { createAttribute('is_npc',0,character.id); } else { createAttribute('is_npc',1,character.id); } Object.entries(elementTypes).forEach(([key,values]) => { if(character[key]) { if(typeof character[key] === 'string') { createAttribute(key,character[key],character.id); } else if(Array.isArray(character[key])) { arrayToAttributes(character[key],character.id,key,values.repeating); debug(`Key: ${key}`); if(key === 'Skills') { character[key].forEach((item) => createAttribute(`static${item[0].toLowerCase()}`,'on',character.id)); } } else { debug(`ERROR ${key} : ${typeof character[key]} : ${character[key]}`); } } }); }; const characterSheetExists = (characterName) => { const querySheet = findObjs({ _type: "character", name: characterName }); if (querySheet.length > 0 ) { return true; } return false; } const initializeCharacterSheet = (characterName) => { const characterObject = createObj("character", { name: characterName, archived: false, }); return characterObject; } const createAttribute = (key,value,characterID) => { if(key === 'undefined' || value === 'undefined' || characterID === 'undefined') { sendChat(`ERROR`,`Error, value missing. Key: ${key} Value: ${value} characterID: ${characterID}`); return; } createObj("attribute", { name: key.toLowerCase(), current: value, characterid: characterID, }); }; const arrayToAttributes = (array,characterID,repeatingName,repeatingAttribute) => { if (array[0] !== undefined && Array.isArray(array[0])) { array.forEach((item) => { if (item[1].indexOf('+') != -1) { const dice = item[1].split("+")[0]; const mod = item[1].split("+")[1]; createAttribute(item[0],convertDiceToNumbers(dice),characterID); createAttribute(`${item[0]}mod`,mod,characterID); } else { createAttribute(item[0],convertDiceToNumbers(item[1]),characterID); } }); return; } if (!repeatingName) { sendChat("ERROR","Element is not returning an array or a repeating section."); return; } else { array.forEach((item) => { const UUID = `repeating_${repeatingName.trim()}_${generateUUID()}_${repeatingAttribute.trim()}`; createAttribute(UUID,item.trim(),characterID); }); } }; const generateUUID = () => { let a = 0; let b = []; let c = (new Date()).getTime() + 0 let d = c === a; a = c; for (var e = new Array(8), f = 7; 0 <= f; f--) { e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".charAt(c % 64); c = Math.floor(c / 64); } c = e.join(""); if (d) { for (f = 11; 0 <= f && 63 === b[f]; f--) { b[f] = 0; } b[f]++; } else { for (f = 0; 12 > f; f++) { b[f] = Math.floor(64 * Math.random()); } } for (f = 0; 12 > f; f++){ c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".charAt(b[f]); } return c; }; const debug = (text) => { if (vocal === 1) { log(`DEBUG: ${text}`); } }; const registerEventHandlers = () => { on('chat:message', handleInput); debug("Event Handlers Registered"); }; return { RegisterEventHandlers: registerEventHandlers }; })(); on("ready",() => { SWImporter.RegisterEventHandlers(); }); Disclaimer: This is entirely third party and unofficial, and is in no way supported or endorsed by Roll20.
Digit.  If this works I shall sacrifice a small halfling child to RNJesus in your name.