From d577176b60e598f18330b5b359a4e12ed2659bc7 Mon Sep 17 00:00:00 2001 From: Dedalus36 Date: Mon, 6 Oct 2025 12:56:21 -0400 Subject: [PATCH 1/3] New API: Grimwild Companion Adding a new, optional, API to accompany the Grimwild character sheet, enabling enhanced roll outputs. --- .../1.4/GrimwildCompanion_v1.4.js | 871 ++++++++++++++++++ Grimwild Companion/GrimwildCompanion_v.1.4.js | 871 ++++++++++++++++++ Grimwild Companion/script.json | 13 + 3 files changed, 1755 insertions(+) create mode 100644 Grimwild Companion/1.4/GrimwildCompanion_v1.4.js create mode 100644 Grimwild Companion/GrimwildCompanion_v.1.4.js create mode 100644 Grimwild Companion/script.json diff --git a/Grimwild Companion/1.4/GrimwildCompanion_v1.4.js b/Grimwild Companion/1.4/GrimwildCompanion_v1.4.js new file mode 100644 index 000000000..aa9b2ff01 --- /dev/null +++ b/Grimwild Companion/1.4/GrimwildCompanion_v1.4.js @@ -0,0 +1,871 @@ +//Grimwild Companion API for Roll20 +//Version: v1.4 — Last updated 10.6.2025 +//Description: Intended to be used alongside the Grimwild character sheet to allow for enhanced rolls. + +var Grimwild = Grimwild || {}; + +on('ready', function() { + log('=== Grimwild Dice System Loaded ==='); + log('Grimwild dice system is ready! v1.4'); +}); + +on('chat:message', function(msg) { + if (msg.type !== 'api') return; + + if (msg.content.indexOf('!grimwild') === 0) { + Grimwild.handleGrimwildRoll(msg); + } + + if (msg.content.indexOf('!grimpool') === 0) { + Grimwild.handlePoolRoll(msg); + } + + if (msg.content.indexOf('!grimpowerpool') === 0) { + Grimwild.handlePowerPoolRoll(msg); + } + + if (msg.content.indexOf('!story-menu') === 0) { + Grimwild.handleStoryMenu(msg); + } + + if (msg.content.indexOf('!story-') === 0 && msg.content !== '!story-menu') { + Grimwild.handleStoryOption(msg); + } +}); + +Grimwild.handleGrimwildRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + + if (args[2] === 'menu') { + Grimwild.handleStoryMenu(msg); + return; + } + + let thornCount = 0; + let attributeName = 'Grimwild'; + let difficultyThorns = 0; + let markHarmThorns = 0; + let extraDice = 0; + let wasMarked = false; + let usedSpark = false; + let sparkCount = 0; + let masteryDiceCount = 0; + let usingMastery = false; + let isFrenzied = false; + let frenzyMarks = 0; + let oathDiceCount = 0; + let usingOath = false; + let expertiseDiceCount = 0; + let usingExpertise = false; + let expertiseType = ''; + let prowessDiceCount = 0; + let usingProwess = false; + + for (let i = 2; i < args.length; i++) { + const arg = args[i]; + if (arg.toLowerCase() === 'marked') { + wasMarked = true; + } else if (arg.toLowerCase() === 'spark') { + usedSpark = true; + } else if (arg.toLowerCase() === 'montage') { + attributeName = 'Montage'; + } else if (arg.toLowerCase() === 'dropped') { + attributeName = 'Dropped'; + } else if (arg.toLowerCase() === 'frenzy') { + isFrenzied = true; + } else if (arg.toLowerCase().startsWith('mastery') && arg.length > 7) { + masteryDiceCount = parseInt(arg.slice(7)) || 0; + } else if (arg.toLowerCase() === 'mastery') { + usingMastery = true; + } else if (arg.toLowerCase().startsWith('frenzymarks')) { + frenzyMarks = parseInt(arg.slice(11)) || 0; + } else if (arg.toLowerCase().startsWith('oath') && arg.length > 4) { + oathDiceCount = parseInt(arg.slice(4)) || 0; + } else if (arg.toLowerCase() === 'oath') { + usingOath = true; + } else if (arg.toLowerCase().startsWith('expertise') && arg.length > 9) { + expertiseDiceCount = parseInt(arg.slice(9)) || 0; + } else if (arg.toLowerCase() === 'expertise') { + usingExpertise = true; + if (i + 1 < args.length) { + expertiseType = args[i + 1]; + i++; + } + } else if (arg.toLowerCase().startsWith('prowess') && arg.length > 7) { + prowessDiceCount = parseInt(arg.slice(7)) || 0; + } else if (arg.toLowerCase() === 'prowess') { + usingProwess = true; + } else if (arg.toLowerCase().startsWith('t')) { + thornCount = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('d')) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('m')) { + markHarmThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('e')) { + extraDice = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('s')) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else { + attributeName = arg; + } + } + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimwild [pool size] [thorns] [attribute]'); + return; + } + + const mainDiceCount = poolSize; + const sparkDiceCount = usedSpark ? sparkCount : 0; + + const mainDice = []; + for (let i = 0; i < mainDiceCount; i++) { + mainDice.push(randomInteger(6)); + } + + const sparkDice = []; + for (let i = 0; i < sparkDiceCount; i++) { + sparkDice.push(randomInteger(6)); + } + + const masteryDice = []; + let masteryTriggeredCritical = false; + let masteryTriggeredSpark = false; + if (usingMastery && masteryDiceCount > 0) { + for (let i = 0; i < masteryDiceCount; i++) { + const masteryRoll = randomInteger(6); + masteryDice.push(masteryRoll); + if (masteryRoll === 6) { + masteryTriggeredCritical = true; + } + } + } + + const oathDice = []; + if (usingOath && oathDiceCount > 0) { + for (let i = 0; i < oathDiceCount; i++) { + const oathRoll = randomInteger(6); + oathDice.push(oathRoll); + } + } + + const expertiseDice = []; + if (usingExpertise && expertiseDiceCount > 0) { + for (let i = 0; i < expertiseDiceCount; i++) { + const expertiseRoll = randomInteger(6); + expertiseDice.push(expertiseRoll); + } + } + + const prowessDice = []; + if (usingProwess && prowessDiceCount > 0) { + for (let i = 0; i < prowessDiceCount; i++) { + const prowessRoll = randomInteger(6); + prowessDice.push(prowessRoll); + } + } + + const frenzyDice = []; + if (isFrenzied && frenzyMarks > 0) { + for (let i = 0; i < frenzyMarks; i++) { + const frenzyRoll = randomInteger(6); + frenzyDice.push(frenzyRoll); + } + } + + const allDice = [...mainDice, ...sparkDice]; + const allDiceIncludingMastery = [...allDice, ...masteryDice, ...oathDice, ...expertiseDice, ...prowessDice]; + const highest = Math.max(...allDiceIncludingMastery); + const perfectCount = allDiceIncludingMastery.filter(d => d === 6).length; + const normalPerfectCount = allDice.filter(d => d === 6).length; + + if (masteryTriggeredCritical) { + if (normalPerfectCount > 0) { + masteryTriggeredSpark = true; + } + } + + let difficultyThornResults = []; + let markHarmThornResults = []; + let totalThornCuts = 0; + let displayMarkHarmThorns = markHarmThorns; + + if (isFrenzied && markHarmThorns > 0) { + displayMarkHarmThorns = markHarmThorns; + markHarmThorns = 0; + } + + if (difficultyThorns > 0) { + for (let i = 0; i < difficultyThorns; i++) { + const thornRoll = randomInteger(8); + difficultyThornResults.push(thornRoll); + if (thornRoll >= 7) { + totalThornCuts++; + } + } + } + + if (displayMarkHarmThorns > 0) { + for (let i = 0; i < displayMarkHarmThorns; i++) { + const thornRoll = randomInteger(8); + markHarmThornResults.push(thornRoll); + if (thornRoll >= 7 && !isFrenzied) { + totalThornCuts++; + } + } + } + + let baseResult; + let isCritical = false; + + if (masteryTriggeredCritical || perfectCount >= 2) { + baseResult = 'critical'; + isCritical = true; + } else if (highest === 6) { + baseResult = 'perfect'; + } else if (highest >= 4) { + baseResult = 'messy'; + } else { + baseResult = 'grim'; + } + + let finalResult = baseResult; + if (!isCritical && totalThornCuts > 0) { + if (baseResult === 'perfect') { + finalResult = totalThornCuts >= 1 ? 'messy' : 'perfect'; + if (totalThornCuts >= 2) finalResult = 'grim'; + if (totalThornCuts >= 3) finalResult = 'disaster'; + } else if (baseResult === 'messy') { + finalResult = totalThornCuts >= 1 ? 'grim' : 'messy'; + if (totalThornCuts >= 2) finalResult = 'disaster'; + } else if (baseResult === 'grim') { + finalResult = totalThornCuts >= 1 ? 'disaster' : 'grim'; + } + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'It\'s the optimal situation.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You do it, and choose a bonus.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You remain in the scene!' + }; + } else { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus: greater effect, secondary effect, or setup.' + }; + } + break; + case 'perfect': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'It\'s an ideal situation.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You did it, avoiding trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You\'re out of the scene.' + }; + } else { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + } + break; + case 'messy': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'It\'s okay, but there\'s a catch.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You did it, but with trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You\'re out of the scene, and it\'s bad (e.g., 4d dying, broken leg, trauma).' + }; + } else { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + } + break; + case 'grim': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'Not good, and there\'s trouble.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You failed, and found trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You\'re out of the scene, and forever changed (e.g., death, insanity, morality shift).' + }; + } else { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + } + break; + case 'disaster': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } + break; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + rollString += ` {{rollname=${attributeName}}}`; + rollString += ` {{pool=1}}`; + rollString += ` {{poolname=${attributeName}}}`; + + for (let i = 0; i < mainDice.length; i++) { + rollString += ` {{dice${i+1}=${mainDice[i]}}}`; + } + + if (usingMastery && masteryDice.length > 0) { + rollString += ` {{mastery=1}}`; + for (let i = 0; i < masteryDice.length; i++) { + rollString += ` {{mastery${i+1}=${masteryDice[i]}}}`; + } + } + + if (usingOath && oathDice.length > 0) { + rollString += ` {{oath=1}}`; + for (let i = 0; i < oathDice.length; i++) { + rollString += ` {{oath${i+1}=${oathDice[i]}}}`; + } + } + + if (usingExpertise && expertiseDice.length > 0) { + rollString += ` {{expertise=1}}`; + for (let i = 0; i < expertiseDice.length; i++) { + rollString += ` {{expertise${i+1}=${expertiseDice[i]}}}`; + } + } + + if (usingProwess && prowessDice.length > 0) { + rollString += ` {{prowess=1}}`; + for (let i = 0; i < prowessDice.length; i++) { + rollString += ` {{prowess${i+1}=${prowessDice[i]}}}`; + } + } + + if (usedSpark && sparkDice.length > 0) { + rollString += ` {{spark=1}}`; + for (let i = 0; i < sparkDice.length; i++) { + rollString += ` {{spark${i+1}=${sparkDice[i]}}}`; + } + } + + if (difficultyThornResults.length > 0 || markHarmThornResults.length > 0) { + if (difficultyThornResults.length > 0) { + rollString += ` {{thorns=1}}`; + + for (let i = 0; i < difficultyThornResults.length; i++) { + rollString += ` {{thorn${i+1}=${difficultyThornResults[i]}}}`; + } + + if (markHarmThornResults.length > 0) { + rollString += ` {{markharm=1}}`; + for (let i = 0; i < markHarmThornResults.length; i++) { + rollString += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } else { + rollString += ` {{markharmonly=1}}`; + for (let i = 0; i < markHarmThornResults.length; i++) { + rollString += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=${resultDetails.resultname}}}`; + rollString += ` {{resulttext=${resultDetails.resulttext}}}`; + + let notifications = []; + + if (usedSpark) { + notifications.push(`${sparkCount} Spark used`); + } + + if (usingMastery) { + notifications.push(`Weapon Mastery adds ${masteryDiceCount}d`); + } + + if (masteryTriggeredSpark) { + notifications.push(`Take 1 Spark from Weapon Mastery`); + } + + if (usingOath) { + notifications.push(`Oath grants +1d and resists harm`); + } + + if (usingExpertise && expertiseType) { + const capitalizedType = expertiseType.charAt(0).toUpperCase() + expertiseType.slice(1); + notifications.push(`${capitalizedType} Expertise used`); + } + + if (usingProwess) { + notifications.push(`Prowess used`); + } + + if (isFrenzied && frenzyMarks > 0) { + notifications.push(`Marks provide +${frenzyMarks}d`); + } + + if (isFrenzied && (difficultyThornResults.length > 0 || markHarmThornResults.length > 0)) { + notifications.push(`Marks and Harm ignored`); + } + + if (!isFrenzied && totalThornCuts > 0) { + notifications.push(`${totalThornCuts} Thorn cut${totalThornCuts > 1 ? 's' : ''}`); + } + + if (wasMarked) { + notifications.push(attributeName + ' Mark cleared'); + } + + if (notifications.length > 0) { + rollString += ` {{status=${notifications.join('
')}}}`; + } + + sendChat('Grimwild', rollString); +}; + +Grimwild.handlePoolRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + const customName = args.slice(2).join(' '); + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimpool [pool size 1-12] [optional custom name]'); + return; + } + + const dice = []; + for (let i = 0; i < poolSize; i++) { + dice.push(randomInteger(6)); + } + + const drops = dice.filter(d => d <= 3).length; + const remaining = poolSize - drops; + + const isPowerPool = false; + + let specialMessage = ''; + if (remaining === 0) { + specialMessage = '

Event occurs, situation ends, or resource is lost.'; + } else if (drops === 0 && poolSize === 1) { + specialMessage = '

You can push yourself or spend suspense to deplete the pool!'; + } else if (drops === 0) { + specialMessage = '

No dice dropped, take a secondary effect if triggered by a messy or perfect action.'; + } + + const statusText = `${drops} dice dropped from pool
`; + + let thornResults = ''; + let sparkResults = ''; + let powerGrimwildResult = ''; + + if (isPowerPool) { + const highest = Math.max(...dice); + const perfectCount = dice.filter(d => d === 6).length; + + let finalResult; + if (perfectCount >= 2) { + finalResult = 'critical'; + } else if (highest === 6) { + finalResult = 'perfect'; + } else if (highest >= 4) { + finalResult = 'messy'; + } else { + finalResult = 'grim'; + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus.' + }; + break; + case 'perfect': + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + break; + case 'messy': + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + break; + case 'grim': + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + break; + } + + powerGrimwildResult = ` {{powerresult=1}} {{powerresultname=${resultDetails.resultname}}} {{powerresulttext=${resultDetails.resulttext}}}`; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + if (customName) { + rollString += ` {{rollname=${customName}}}`; + rollString += ` {{poolname=${customName} (${poolSize}d)}}`; + } else { + rollString += ` {{poolname=Pool (${poolSize}d)}}`; + } + rollString += ` {{pool=1}}`; + rollString += ` {{poolroll=1}}`; + + for (let i = 0; i < dice.length; i++) { + rollString += ` {{dice${i+1}=${dice[i]}}}`; + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=POOL RESULT}}`; + rollString += ` {{resulttext=${remaining}d Remaining${specialMessage}}}`; + rollString += ` {{status=${statusText}}}`; + rollString += thornResults; + rollString += sparkResults; + rollString += powerGrimwildResult; + + sendChat('Grimwild', rollString); +}; + +Grimwild.handleStoryMenu = function(msg) { + const playerName = msg.who.replace(' (GM)', ''); + const args = msg.content.split(' '); + + let difficultyThorns = 0; + let markHarmThorns = 0; + let extraDice = 0; + let sparkCount = 0; + let usingSpark = false; + + for (let i = 2; i < args.length; i++) { + const arg = args[i]; + if (arg.toLowerCase().startsWith('d')) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('m')) { + markHarmThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('e')) { + extraDice = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('s')) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase() === 'spark') { + usingSpark = true; + } + } + + const totalThorns = difficultyThorns + markHarmThorns; + const sparkText = usingSpark ? ' spark' : ''; + + let menuOutput = '/w "' + playerName + '" '; + menuOutput += '&{template:grimwild-menu} '; + menuOutput += '{{rollname=' + playerName + ' | Story Choice}} '; + menuOutput += '{{menu_title=SELECT A STORY ROLL}} '; + + let buttonOptions = ''; + buttonOptions += `[Bad Odds | 1d](!grimwild ${1 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Even Odds | 2d](!grimwild ${2 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Good Odds |3d](!grimwild ${3 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Montage | 2d](!grimwild ${2 + extraDice} t${totalThorns} Montage d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})`; + + menuOutput += `{{menu_options=${buttonOptions}}}`; + + sendChat('', menuOutput); +}; + +Grimwild.handleStoryOption = function(msg) { + const option = msg.content.substring(1); + let diceCount = 1; + let rollName = 'Story'; + + switch(option) { + case 'story-bad': diceCount = 1; break; + case 'story-even': diceCount = 2; break; + case 'story-good': diceCount = 3; break; + case 'story-montage': diceCount = 2; rollName = 'Montage'; break; + } + + const grimwildCommand = `!grimwild ${diceCount} t0 ${rollName} d0 m0 e0`; + Grimwild.handleGrimwildRoll({ + content: grimwildCommand, + who: msg.who, + type: 'api' + }); +}; + +Grimwild.handlePowerPoolRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + + let customName = ''; + let difficultyThorns = 0; + let bloodiedThorns = 0; + let rattledThorns = 0; + let usingSpark = false; + + let sparkCount = 0; + + let nameArgs = []; + for (let i = 2; i < args.length; i++) { + const arg = args[i].toLowerCase(); + + if (/^t\d+$/.test(arg)) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (/^b\d+$/.test(arg)) { + bloodiedThorns = parseInt(arg.slice(1)) || 0; + } else if (/^r\d+$/.test(arg)) { + rattledThorns = parseInt(arg.slice(1)) || 0; + } else if (/^s\d+$/.test(arg)) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else if (arg === 'spark') { + usingSpark = true; + } else { + nameArgs.push(args[i]); + } + } + + customName = nameArgs.join(' '); + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimpowerpool [pool size 1-12] [optional custom name]'); + return; + } + + const totalPoolSize = poolSize + (usingSpark ? sparkCount : 0); + const dice = []; + for (let i = 0; i < totalPoolSize; i++) { + dice.push(randomInteger(6)); + } + + const drops = dice.filter(d => d <= 3).length; + const remaining = totalPoolSize - drops; + + let specialMessage = ''; + if (remaining === 0) { + specialMessage = '

Power pool is depleted.'; + } + + const statusText = `${drops} dice dropped from pool

`; + + const highest = Math.max(...dice); + const perfectCount = dice.filter(d => d === 6).length; + + let difficultyThornResults = []; + let markHarmThornResults = []; + let totalThornCuts = 0; + let thornResults = ''; + + if (difficultyThorns > 0) { + for (let i = 0; i < difficultyThorns; i++) { + const thornRoll = randomInteger(8); + difficultyThornResults.push(thornRoll); + if (thornRoll >= 7) totalThornCuts++; + } + } + + const markHarmThorns = bloodiedThorns + rattledThorns; + if (markHarmThorns > 0) { + for (let i = 0; i < markHarmThorns; i++) { + const thornRoll = randomInteger(8); + markHarmThornResults.push(thornRoll); + if (thornRoll >= 7) totalThornCuts++; + } + } + + if (difficultyThornResults.length > 0 || markHarmThornResults.length > 0) { + if (difficultyThornResults.length > 0) { + thornResults += ' {{thorns=1}}'; + for (let i = 0; i < difficultyThornResults.length; i++) { + thornResults += ` {{thorn${i+1}=${difficultyThornResults[i]}}}`; + } + + if (markHarmThornResults.length > 0) { + thornResults += ' {{markharm=1}}'; + for (let i = 0; i < markHarmThornResults.length; i++) { + thornResults += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } else { + thornResults += ' {{markharmonly=1}}'; + for (let i = 0; i < markHarmThornResults.length; i++) { + thornResults += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } + + let baseResult; + let isCritical = false; + + if (perfectCount >= 2) { + baseResult = 'critical'; + isCritical = true; + } else if (highest === 6) { + baseResult = 'perfect'; + } else if (highest >= 4) { + baseResult = 'messy'; + } else { + baseResult = 'grim'; + } + + let finalResult = baseResult; + if (!isCritical && totalThornCuts > 0) { + if (baseResult === 'perfect') { + finalResult = totalThornCuts >= 1 ? 'messy' : 'perfect'; + if (totalThornCuts >= 2) finalResult = 'grim'; + if (totalThornCuts >= 3) finalResult = 'disaster'; + } else if (baseResult === 'messy') { + finalResult = totalThornCuts >= 1 ? 'grim' : 'messy'; + if (totalThornCuts >= 2) finalResult = 'disaster'; + } else if (baseResult === 'grim') { + finalResult = totalThornCuts >= 1 ? 'disaster' : 'grim'; + } + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus.' + }; + break; + case 'perfect': + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + break; + case 'messy': + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + break; + case 'grim': + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + break; + case 'disaster': + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + break; + } + + let powerNotifications = []; + if (usingSpark) { + powerNotifications.push('Spark used'); + } + if (totalThornCuts > 0) { + powerNotifications.push(`${totalThornCuts} thorn cut${totalThornCuts > 1 ? 's' : ''}`); + } + + let powerStatusText = ''; + if (powerNotifications.length > 0) { + powerStatusText = ` {{powerstatus=${powerNotifications.join('
')}}}`; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + + const displaySize = usingSpark ? `${poolSize}d+${sparkCount}d SPARK` : `${poolSize}d`; + if (customName) { + rollString += ` {{rollname=${customName} Power}}`; + rollString += ` {{poolname=${customName} (${displaySize})}}`; + } else { + rollString += ` {{rollname=Power Pool}}`; + rollString += ` {{poolname=Power Pool (${displaySize})}}`; + } + rollString += ` {{pool=1}}`; + rollString += ` {{poolroll=1}}`; + rollString += ` {{powerroll=1}}`; + + for (let i = 0; i < totalPoolSize; i++) { + rollString += ` {{dice${i+1}=${dice[i]}}}`; + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=POOL RESULT}}`; + rollString += ` {{resulttext=${remaining}d Remaining${specialMessage}}}`; + rollString += ` {{status=${statusText}}}`; + + rollString += thornResults; + + rollString += ` {{powerresult=1}}`; + rollString += ` {{powerresultname=${resultDetails.resultname}}}`; + rollString += ` {{powerresulttext=${resultDetails.resulttext}}}`; + rollString += powerStatusText; + + sendChat('Grimwild', rollString); +}; \ No newline at end of file diff --git a/Grimwild Companion/GrimwildCompanion_v.1.4.js b/Grimwild Companion/GrimwildCompanion_v.1.4.js new file mode 100644 index 000000000..aa9b2ff01 --- /dev/null +++ b/Grimwild Companion/GrimwildCompanion_v.1.4.js @@ -0,0 +1,871 @@ +//Grimwild Companion API for Roll20 +//Version: v1.4 — Last updated 10.6.2025 +//Description: Intended to be used alongside the Grimwild character sheet to allow for enhanced rolls. + +var Grimwild = Grimwild || {}; + +on('ready', function() { + log('=== Grimwild Dice System Loaded ==='); + log('Grimwild dice system is ready! v1.4'); +}); + +on('chat:message', function(msg) { + if (msg.type !== 'api') return; + + if (msg.content.indexOf('!grimwild') === 0) { + Grimwild.handleGrimwildRoll(msg); + } + + if (msg.content.indexOf('!grimpool') === 0) { + Grimwild.handlePoolRoll(msg); + } + + if (msg.content.indexOf('!grimpowerpool') === 0) { + Grimwild.handlePowerPoolRoll(msg); + } + + if (msg.content.indexOf('!story-menu') === 0) { + Grimwild.handleStoryMenu(msg); + } + + if (msg.content.indexOf('!story-') === 0 && msg.content !== '!story-menu') { + Grimwild.handleStoryOption(msg); + } +}); + +Grimwild.handleGrimwildRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + + if (args[2] === 'menu') { + Grimwild.handleStoryMenu(msg); + return; + } + + let thornCount = 0; + let attributeName = 'Grimwild'; + let difficultyThorns = 0; + let markHarmThorns = 0; + let extraDice = 0; + let wasMarked = false; + let usedSpark = false; + let sparkCount = 0; + let masteryDiceCount = 0; + let usingMastery = false; + let isFrenzied = false; + let frenzyMarks = 0; + let oathDiceCount = 0; + let usingOath = false; + let expertiseDiceCount = 0; + let usingExpertise = false; + let expertiseType = ''; + let prowessDiceCount = 0; + let usingProwess = false; + + for (let i = 2; i < args.length; i++) { + const arg = args[i]; + if (arg.toLowerCase() === 'marked') { + wasMarked = true; + } else if (arg.toLowerCase() === 'spark') { + usedSpark = true; + } else if (arg.toLowerCase() === 'montage') { + attributeName = 'Montage'; + } else if (arg.toLowerCase() === 'dropped') { + attributeName = 'Dropped'; + } else if (arg.toLowerCase() === 'frenzy') { + isFrenzied = true; + } else if (arg.toLowerCase().startsWith('mastery') && arg.length > 7) { + masteryDiceCount = parseInt(arg.slice(7)) || 0; + } else if (arg.toLowerCase() === 'mastery') { + usingMastery = true; + } else if (arg.toLowerCase().startsWith('frenzymarks')) { + frenzyMarks = parseInt(arg.slice(11)) || 0; + } else if (arg.toLowerCase().startsWith('oath') && arg.length > 4) { + oathDiceCount = parseInt(arg.slice(4)) || 0; + } else if (arg.toLowerCase() === 'oath') { + usingOath = true; + } else if (arg.toLowerCase().startsWith('expertise') && arg.length > 9) { + expertiseDiceCount = parseInt(arg.slice(9)) || 0; + } else if (arg.toLowerCase() === 'expertise') { + usingExpertise = true; + if (i + 1 < args.length) { + expertiseType = args[i + 1]; + i++; + } + } else if (arg.toLowerCase().startsWith('prowess') && arg.length > 7) { + prowessDiceCount = parseInt(arg.slice(7)) || 0; + } else if (arg.toLowerCase() === 'prowess') { + usingProwess = true; + } else if (arg.toLowerCase().startsWith('t')) { + thornCount = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('d')) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('m')) { + markHarmThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('e')) { + extraDice = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('s')) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else { + attributeName = arg; + } + } + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimwild [pool size] [thorns] [attribute]'); + return; + } + + const mainDiceCount = poolSize; + const sparkDiceCount = usedSpark ? sparkCount : 0; + + const mainDice = []; + for (let i = 0; i < mainDiceCount; i++) { + mainDice.push(randomInteger(6)); + } + + const sparkDice = []; + for (let i = 0; i < sparkDiceCount; i++) { + sparkDice.push(randomInteger(6)); + } + + const masteryDice = []; + let masteryTriggeredCritical = false; + let masteryTriggeredSpark = false; + if (usingMastery && masteryDiceCount > 0) { + for (let i = 0; i < masteryDiceCount; i++) { + const masteryRoll = randomInteger(6); + masteryDice.push(masteryRoll); + if (masteryRoll === 6) { + masteryTriggeredCritical = true; + } + } + } + + const oathDice = []; + if (usingOath && oathDiceCount > 0) { + for (let i = 0; i < oathDiceCount; i++) { + const oathRoll = randomInteger(6); + oathDice.push(oathRoll); + } + } + + const expertiseDice = []; + if (usingExpertise && expertiseDiceCount > 0) { + for (let i = 0; i < expertiseDiceCount; i++) { + const expertiseRoll = randomInteger(6); + expertiseDice.push(expertiseRoll); + } + } + + const prowessDice = []; + if (usingProwess && prowessDiceCount > 0) { + for (let i = 0; i < prowessDiceCount; i++) { + const prowessRoll = randomInteger(6); + prowessDice.push(prowessRoll); + } + } + + const frenzyDice = []; + if (isFrenzied && frenzyMarks > 0) { + for (let i = 0; i < frenzyMarks; i++) { + const frenzyRoll = randomInteger(6); + frenzyDice.push(frenzyRoll); + } + } + + const allDice = [...mainDice, ...sparkDice]; + const allDiceIncludingMastery = [...allDice, ...masteryDice, ...oathDice, ...expertiseDice, ...prowessDice]; + const highest = Math.max(...allDiceIncludingMastery); + const perfectCount = allDiceIncludingMastery.filter(d => d === 6).length; + const normalPerfectCount = allDice.filter(d => d === 6).length; + + if (masteryTriggeredCritical) { + if (normalPerfectCount > 0) { + masteryTriggeredSpark = true; + } + } + + let difficultyThornResults = []; + let markHarmThornResults = []; + let totalThornCuts = 0; + let displayMarkHarmThorns = markHarmThorns; + + if (isFrenzied && markHarmThorns > 0) { + displayMarkHarmThorns = markHarmThorns; + markHarmThorns = 0; + } + + if (difficultyThorns > 0) { + for (let i = 0; i < difficultyThorns; i++) { + const thornRoll = randomInteger(8); + difficultyThornResults.push(thornRoll); + if (thornRoll >= 7) { + totalThornCuts++; + } + } + } + + if (displayMarkHarmThorns > 0) { + for (let i = 0; i < displayMarkHarmThorns; i++) { + const thornRoll = randomInteger(8); + markHarmThornResults.push(thornRoll); + if (thornRoll >= 7 && !isFrenzied) { + totalThornCuts++; + } + } + } + + let baseResult; + let isCritical = false; + + if (masteryTriggeredCritical || perfectCount >= 2) { + baseResult = 'critical'; + isCritical = true; + } else if (highest === 6) { + baseResult = 'perfect'; + } else if (highest >= 4) { + baseResult = 'messy'; + } else { + baseResult = 'grim'; + } + + let finalResult = baseResult; + if (!isCritical && totalThornCuts > 0) { + if (baseResult === 'perfect') { + finalResult = totalThornCuts >= 1 ? 'messy' : 'perfect'; + if (totalThornCuts >= 2) finalResult = 'grim'; + if (totalThornCuts >= 3) finalResult = 'disaster'; + } else if (baseResult === 'messy') { + finalResult = totalThornCuts >= 1 ? 'grim' : 'messy'; + if (totalThornCuts >= 2) finalResult = 'disaster'; + } else if (baseResult === 'grim') { + finalResult = totalThornCuts >= 1 ? 'disaster' : 'grim'; + } + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'It\'s the optimal situation.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You do it, and choose a bonus.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You remain in the scene!' + }; + } else { + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus: greater effect, secondary effect, or setup.' + }; + } + break; + case 'perfect': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'It\'s an ideal situation.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You did it, avoiding trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You\'re out of the scene.' + }; + } else { + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + } + break; + case 'messy': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'It\'s okay, but there\'s a catch.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You did it, but with trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You\'re out of the scene, and it\'s bad (e.g., 4d dying, broken leg, trauma).' + }; + } else { + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + } + break; + case 'grim': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'Not good, and there\'s trouble.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You failed, and found trouble.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You\'re out of the scene, and forever changed (e.g., death, insanity, morality shift).' + }; + } else { + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + } + break; + case 'disaster': + if (attributeName === 'Story') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else if (attributeName === 'Montage') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else if (attributeName === 'Dropped') { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } else { + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + } + break; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + rollString += ` {{rollname=${attributeName}}}`; + rollString += ` {{pool=1}}`; + rollString += ` {{poolname=${attributeName}}}`; + + for (let i = 0; i < mainDice.length; i++) { + rollString += ` {{dice${i+1}=${mainDice[i]}}}`; + } + + if (usingMastery && masteryDice.length > 0) { + rollString += ` {{mastery=1}}`; + for (let i = 0; i < masteryDice.length; i++) { + rollString += ` {{mastery${i+1}=${masteryDice[i]}}}`; + } + } + + if (usingOath && oathDice.length > 0) { + rollString += ` {{oath=1}}`; + for (let i = 0; i < oathDice.length; i++) { + rollString += ` {{oath${i+1}=${oathDice[i]}}}`; + } + } + + if (usingExpertise && expertiseDice.length > 0) { + rollString += ` {{expertise=1}}`; + for (let i = 0; i < expertiseDice.length; i++) { + rollString += ` {{expertise${i+1}=${expertiseDice[i]}}}`; + } + } + + if (usingProwess && prowessDice.length > 0) { + rollString += ` {{prowess=1}}`; + for (let i = 0; i < prowessDice.length; i++) { + rollString += ` {{prowess${i+1}=${prowessDice[i]}}}`; + } + } + + if (usedSpark && sparkDice.length > 0) { + rollString += ` {{spark=1}}`; + for (let i = 0; i < sparkDice.length; i++) { + rollString += ` {{spark${i+1}=${sparkDice[i]}}}`; + } + } + + if (difficultyThornResults.length > 0 || markHarmThornResults.length > 0) { + if (difficultyThornResults.length > 0) { + rollString += ` {{thorns=1}}`; + + for (let i = 0; i < difficultyThornResults.length; i++) { + rollString += ` {{thorn${i+1}=${difficultyThornResults[i]}}}`; + } + + if (markHarmThornResults.length > 0) { + rollString += ` {{markharm=1}}`; + for (let i = 0; i < markHarmThornResults.length; i++) { + rollString += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } else { + rollString += ` {{markharmonly=1}}`; + for (let i = 0; i < markHarmThornResults.length; i++) { + rollString += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=${resultDetails.resultname}}}`; + rollString += ` {{resulttext=${resultDetails.resulttext}}}`; + + let notifications = []; + + if (usedSpark) { + notifications.push(`${sparkCount} Spark used`); + } + + if (usingMastery) { + notifications.push(`Weapon Mastery adds ${masteryDiceCount}d`); + } + + if (masteryTriggeredSpark) { + notifications.push(`Take 1 Spark from Weapon Mastery`); + } + + if (usingOath) { + notifications.push(`Oath grants +1d and resists harm`); + } + + if (usingExpertise && expertiseType) { + const capitalizedType = expertiseType.charAt(0).toUpperCase() + expertiseType.slice(1); + notifications.push(`${capitalizedType} Expertise used`); + } + + if (usingProwess) { + notifications.push(`Prowess used`); + } + + if (isFrenzied && frenzyMarks > 0) { + notifications.push(`Marks provide +${frenzyMarks}d`); + } + + if (isFrenzied && (difficultyThornResults.length > 0 || markHarmThornResults.length > 0)) { + notifications.push(`Marks and Harm ignored`); + } + + if (!isFrenzied && totalThornCuts > 0) { + notifications.push(`${totalThornCuts} Thorn cut${totalThornCuts > 1 ? 's' : ''}`); + } + + if (wasMarked) { + notifications.push(attributeName + ' Mark cleared'); + } + + if (notifications.length > 0) { + rollString += ` {{status=${notifications.join('
')}}}`; + } + + sendChat('Grimwild', rollString); +}; + +Grimwild.handlePoolRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + const customName = args.slice(2).join(' '); + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimpool [pool size 1-12] [optional custom name]'); + return; + } + + const dice = []; + for (let i = 0; i < poolSize; i++) { + dice.push(randomInteger(6)); + } + + const drops = dice.filter(d => d <= 3).length; + const remaining = poolSize - drops; + + const isPowerPool = false; + + let specialMessage = ''; + if (remaining === 0) { + specialMessage = '

Event occurs, situation ends, or resource is lost.'; + } else if (drops === 0 && poolSize === 1) { + specialMessage = '

You can push yourself or spend suspense to deplete the pool!'; + } else if (drops === 0) { + specialMessage = '

No dice dropped, take a secondary effect if triggered by a messy or perfect action.'; + } + + const statusText = `${drops} dice dropped from pool
`; + + let thornResults = ''; + let sparkResults = ''; + let powerGrimwildResult = ''; + + if (isPowerPool) { + const highest = Math.max(...dice); + const perfectCount = dice.filter(d => d === 6).length; + + let finalResult; + if (perfectCount >= 2) { + finalResult = 'critical'; + } else if (highest === 6) { + finalResult = 'perfect'; + } else if (highest >= 4) { + finalResult = 'messy'; + } else { + finalResult = 'grim'; + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus.' + }; + break; + case 'perfect': + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + break; + case 'messy': + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + break; + case 'grim': + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + break; + } + + powerGrimwildResult = ` {{powerresult=1}} {{powerresultname=${resultDetails.resultname}}} {{powerresulttext=${resultDetails.resulttext}}}`; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + if (customName) { + rollString += ` {{rollname=${customName}}}`; + rollString += ` {{poolname=${customName} (${poolSize}d)}}`; + } else { + rollString += ` {{poolname=Pool (${poolSize}d)}}`; + } + rollString += ` {{pool=1}}`; + rollString += ` {{poolroll=1}}`; + + for (let i = 0; i < dice.length; i++) { + rollString += ` {{dice${i+1}=${dice[i]}}}`; + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=POOL RESULT}}`; + rollString += ` {{resulttext=${remaining}d Remaining${specialMessage}}}`; + rollString += ` {{status=${statusText}}}`; + rollString += thornResults; + rollString += sparkResults; + rollString += powerGrimwildResult; + + sendChat('Grimwild', rollString); +}; + +Grimwild.handleStoryMenu = function(msg) { + const playerName = msg.who.replace(' (GM)', ''); + const args = msg.content.split(' '); + + let difficultyThorns = 0; + let markHarmThorns = 0; + let extraDice = 0; + let sparkCount = 0; + let usingSpark = false; + + for (let i = 2; i < args.length; i++) { + const arg = args[i]; + if (arg.toLowerCase().startsWith('d')) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('m')) { + markHarmThorns = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('e')) { + extraDice = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase().startsWith('s')) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else if (arg.toLowerCase() === 'spark') { + usingSpark = true; + } + } + + const totalThorns = difficultyThorns + markHarmThorns; + const sparkText = usingSpark ? ' spark' : ''; + + let menuOutput = '/w "' + playerName + '" '; + menuOutput += '&{template:grimwild-menu} '; + menuOutput += '{{rollname=' + playerName + ' | Story Choice}} '; + menuOutput += '{{menu_title=SELECT A STORY ROLL}} '; + + let buttonOptions = ''; + buttonOptions += `[Bad Odds | 1d](!grimwild ${1 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Even Odds | 2d](!grimwild ${2 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Good Odds |3d](!grimwild ${3 + extraDice} t${totalThorns} Story d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})
`; + buttonOptions += `[Montage | 2d](!grimwild ${2 + extraDice} t${totalThorns} Montage d${difficultyThorns} m${markHarmThorns} e${extraDice} s${sparkCount}${sparkText})`; + + menuOutput += `{{menu_options=${buttonOptions}}}`; + + sendChat('', menuOutput); +}; + +Grimwild.handleStoryOption = function(msg) { + const option = msg.content.substring(1); + let diceCount = 1; + let rollName = 'Story'; + + switch(option) { + case 'story-bad': diceCount = 1; break; + case 'story-even': diceCount = 2; break; + case 'story-good': diceCount = 3; break; + case 'story-montage': diceCount = 2; rollName = 'Montage'; break; + } + + const grimwildCommand = `!grimwild ${diceCount} t0 ${rollName} d0 m0 e0`; + Grimwild.handleGrimwildRoll({ + content: grimwildCommand, + who: msg.who, + type: 'api' + }); +}; + +Grimwild.handlePowerPoolRoll = function(msg) { + const args = msg.content.split(' '); + const poolSize = parseInt(args[1]); + + let customName = ''; + let difficultyThorns = 0; + let bloodiedThorns = 0; + let rattledThorns = 0; + let usingSpark = false; + + let sparkCount = 0; + + let nameArgs = []; + for (let i = 2; i < args.length; i++) { + const arg = args[i].toLowerCase(); + + if (/^t\d+$/.test(arg)) { + difficultyThorns = parseInt(arg.slice(1)) || 0; + } else if (/^b\d+$/.test(arg)) { + bloodiedThorns = parseInt(arg.slice(1)) || 0; + } else if (/^r\d+$/.test(arg)) { + rattledThorns = parseInt(arg.slice(1)) || 0; + } else if (/^s\d+$/.test(arg)) { + sparkCount = parseInt(arg.slice(1)) || 0; + } else if (arg === 'spark') { + usingSpark = true; + } else { + nameArgs.push(args[i]); + } + } + + customName = nameArgs.join(' '); + + if (isNaN(poolSize) || poolSize < 1 || poolSize > 12) { + sendChat('Grimwild', '/w "' + msg.who + '" Usage: !grimpowerpool [pool size 1-12] [optional custom name]'); + return; + } + + const totalPoolSize = poolSize + (usingSpark ? sparkCount : 0); + const dice = []; + for (let i = 0; i < totalPoolSize; i++) { + dice.push(randomInteger(6)); + } + + const drops = dice.filter(d => d <= 3).length; + const remaining = totalPoolSize - drops; + + let specialMessage = ''; + if (remaining === 0) { + specialMessage = '

Power pool is depleted.'; + } + + const statusText = `${drops} dice dropped from pool

`; + + const highest = Math.max(...dice); + const perfectCount = dice.filter(d => d === 6).length; + + let difficultyThornResults = []; + let markHarmThornResults = []; + let totalThornCuts = 0; + let thornResults = ''; + + if (difficultyThorns > 0) { + for (let i = 0; i < difficultyThorns; i++) { + const thornRoll = randomInteger(8); + difficultyThornResults.push(thornRoll); + if (thornRoll >= 7) totalThornCuts++; + } + } + + const markHarmThorns = bloodiedThorns + rattledThorns; + if (markHarmThorns > 0) { + for (let i = 0; i < markHarmThorns; i++) { + const thornRoll = randomInteger(8); + markHarmThornResults.push(thornRoll); + if (thornRoll >= 7) totalThornCuts++; + } + } + + if (difficultyThornResults.length > 0 || markHarmThornResults.length > 0) { + if (difficultyThornResults.length > 0) { + thornResults += ' {{thorns=1}}'; + for (let i = 0; i < difficultyThornResults.length; i++) { + thornResults += ` {{thorn${i+1}=${difficultyThornResults[i]}}}`; + } + + if (markHarmThornResults.length > 0) { + thornResults += ' {{markharm=1}}'; + for (let i = 0; i < markHarmThornResults.length; i++) { + thornResults += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } else { + thornResults += ' {{markharmonly=1}}'; + for (let i = 0; i < markHarmThornResults.length; i++) { + thornResults += ` {{mark${i+1}=${markHarmThornResults[i]}}}`; + } + } + } + + let baseResult; + let isCritical = false; + + if (perfectCount >= 2) { + baseResult = 'critical'; + isCritical = true; + } else if (highest === 6) { + baseResult = 'perfect'; + } else if (highest >= 4) { + baseResult = 'messy'; + } else { + baseResult = 'grim'; + } + + let finalResult = baseResult; + if (!isCritical && totalThornCuts > 0) { + if (baseResult === 'perfect') { + finalResult = totalThornCuts >= 1 ? 'messy' : 'perfect'; + if (totalThornCuts >= 2) finalResult = 'grim'; + if (totalThornCuts >= 3) finalResult = 'disaster'; + } else if (baseResult === 'messy') { + finalResult = totalThornCuts >= 1 ? 'grim' : 'messy'; + if (totalThornCuts >= 2) finalResult = 'disaster'; + } else if (baseResult === 'grim') { + finalResult = totalThornCuts >= 1 ? 'disaster' : 'grim'; + } + } + + let resultDetails = {}; + switch(finalResult) { + case 'critical': + resultDetails = { + resultname: 'CRITICAL', + resulttext: 'You did it, choose a bonus.' + }; + break; + case 'perfect': + resultDetails = { + resultname: 'PERFECT', + resulttext: 'You do it, and avoid trouble.' + }; + break; + case 'messy': + resultDetails = { + resultname: 'MESSY', + resulttext: 'You do it, but there\'s trouble.' + }; + break; + case 'grim': + resultDetails = { + resultname: 'GRIM', + resulttext: 'You fail, and there\'s trouble.' + }; + break; + case 'disaster': + resultDetails = { + resultname: 'DISASTER', + resulttext: 'The worst case scenario.' + }; + break; + } + + let powerNotifications = []; + if (usingSpark) { + powerNotifications.push('Spark used'); + } + if (totalThornCuts > 0) { + powerNotifications.push(`${totalThornCuts} thorn cut${totalThornCuts > 1 ? 's' : ''}`); + } + + let powerStatusText = ''; + if (powerNotifications.length > 0) { + powerStatusText = ` {{powerstatus=${powerNotifications.join('
')}}}`; + } + + const playerName = msg.who.replace(' (GM)', ''); + + let rollString = '&{template:grimwild}'; + rollString += ` {{charactername=${playerName}}}`; + + const displaySize = usingSpark ? `${poolSize}d+${sparkCount}d SPARK` : `${poolSize}d`; + if (customName) { + rollString += ` {{rollname=${customName} Power}}`; + rollString += ` {{poolname=${customName} (${displaySize})}}`; + } else { + rollString += ` {{rollname=Power Pool}}`; + rollString += ` {{poolname=Power Pool (${displaySize})}}`; + } + rollString += ` {{pool=1}}`; + rollString += ` {{poolroll=1}}`; + rollString += ` {{powerroll=1}}`; + + for (let i = 0; i < totalPoolSize; i++) { + rollString += ` {{dice${i+1}=${dice[i]}}}`; + } + + rollString += ` {{result=1}}`; + rollString += ` {{resultname=POOL RESULT}}`; + rollString += ` {{resulttext=${remaining}d Remaining${specialMessage}}}`; + rollString += ` {{status=${statusText}}}`; + + rollString += thornResults; + + rollString += ` {{powerresult=1}}`; + rollString += ` {{powerresultname=${resultDetails.resultname}}}`; + rollString += ` {{powerresulttext=${resultDetails.resulttext}}}`; + rollString += powerStatusText; + + sendChat('Grimwild', rollString); +}; \ No newline at end of file diff --git a/Grimwild Companion/script.json b/Grimwild Companion/script.json new file mode 100644 index 000000000..a67904172 --- /dev/null +++ b/Grimwild Companion/script.json @@ -0,0 +1,13 @@ +{ + "name": "Grimwild Companion", + "script": "GrimwildCompanion_v1.4.js", + "previousversions": [], + "version": "1.4", + "description": "This API is designed to be used alongside the Grimwild Roll20 Character Sheet in order to facilitate roll functionality with enhanced designs and autocalculated results.", + "authors": "Ryan K.", + "roll20userid": "539081", + "useroptions": [], + "dependencies": [], + "modifies": [] + "conflicts": [], +} \ No newline at end of file From 8b6dd4f1fc2529665074d5cd92a682d9f0afc31d Mon Sep 17 00:00:00 2001 From: Dedalus36 Date: Tue, 7 Oct 2025 14:04:08 -0400 Subject: [PATCH 2/3] Update script.json --- Grimwild Companion/script.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Grimwild Companion/script.json b/Grimwild Companion/script.json index a67904172..1447e308c 100644 --- a/Grimwild Companion/script.json +++ b/Grimwild Companion/script.json @@ -8,6 +8,6 @@ "roll20userid": "539081", "useroptions": [], "dependencies": [], - "modifies": [] + "modifies": [], "conflicts": [], } \ No newline at end of file From f5c4742e9804fea4342a42fc2c2f9da6d73210ce Mon Sep 17 00:00:00 2001 From: Dedalus36 Date: Wed, 22 Oct 2025 14:50:38 -0400 Subject: [PATCH 3/3] Update script.json --- Grimwild Companion/script.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Grimwild Companion/script.json b/Grimwild Companion/script.json index 1447e308c..65304578b 100644 --- a/Grimwild Companion/script.json +++ b/Grimwild Companion/script.json @@ -8,6 +8,6 @@ "roll20userid": "539081", "useroptions": [], "dependencies": [], - "modifies": [], - "conflicts": [], -} \ No newline at end of file + "modifies": {}, + "conflicts": [] +}