Instantly share code, notes, and snippets.
Last active
June 7, 2026 15:49
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save revilowaldow/3f9cb91efd0bc31cdb4eaeb67fa7b0d4 to your computer and use it in GitHub Desktop.
HAZARD NAVIGATOR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const defaultStatistics = { | |
| Strength: 11, | |
| Finesse: 12, | |
| Intuition: 11, | |
| Intellect: 7 | |
| }; | |
| const characterSkills = { | |
| "Close Quarters Combat": 0, | |
| "Defence": 1, | |
| "Unarmed / Martial Arts": 1, | |
| "Fitness & Conditioning": 0, | |
| "Heavy Weapon Operation": 0, | |
| "Manual Labour": 0, | |
| "Ranged Combat": 0, | |
| "Vehicle Control": 1, | |
| "Survival & Nature Crafting": 0, | |
| "Tinkering & Repair": 0, | |
| "Acrobatics & Body Control": 1, | |
| "Combat Finesse": 1, | |
| "Riding & Animal Control": 0, | |
| "Acting & Performance": 0, | |
| "Deception & Misdirection": 0, | |
| "Eloquence & Oration": 0, | |
| "Art & Music": 0, | |
| "People Reading": 0, | |
| "Arcanothurgy": 0, | |
| "Religion": 0, | |
| "History, Politics & Geography": 0, | |
| "Medicine & Biology": 0, | |
| "Law & Philosophy": 0, | |
| "Engineering, Mathematics & Physics": 0 | |
| }; | |
| const rollOptions = { | |
| "+3": "6d6kh3", | |
| "+2": "5d6kh3", | |
| "+1": "4d6kh3", | |
| "3d6": "3d6", | |
| "-1": "4d6kl3", | |
| "-2": "5d6kl3", | |
| "-3": "6d6kl3" | |
| }; | |
| const statisticModifiers = { | |
| 1: -3, | |
| 2: -3, | |
| 3: -3, | |
| 4: -2, | |
| 5: -1, | |
| 6: 0, | |
| 7: 0, | |
| 8: 1, | |
| 9: 2, | |
| 10: 3, | |
| 11: 3, | |
| 12: 3 | |
| }; | |
| function getStatisticModifier(score) { | |
| return statisticModifiers[score] ?? 0; | |
| } | |
| async function makeRoll(type, modifier, volatilityEnabled, stressLevel, statisticName, statisticScore, skillBonus) { | |
| let formula = rollOptions[type]; | |
| // Calculate component modifiers | |
| const statisticModifier = statisticName ? getStatisticModifier(statisticScore) : 0; | |
| const totalModifiers = modifier + stressLevel + statisticModifier + skillBonus; | |
| // Build modifier expression with components shown and capping handled by Foundry | |
| if (totalModifiers !== 0) { | |
| const components = []; | |
| if (modifier !== 0) components.push(`${modifier}[Custom]`); | |
| if (stressLevel !== 0) components.push(`${stressLevel}[Stress]`); | |
| if (statisticModifier !== 0) components.push(`${statisticModifier}[${statisticName}]`); | |
| if (skillBonus !== 0) components.push(`${skillBonus}[Skill]`); | |
| const componentsStr = components.join(' + '); | |
| formula += ` + min(5, max(-5, ${componentsStr}))`; | |
| } | |
| const roll = await new Roll(formula).evaluate(); | |
| const die = roll.dice[0]; | |
| const keptDice = die.results | |
| .filter(r => !r.discarded) | |
| .map(r => r.result); | |
| const ones = keptDice.filter(v => v === 1).length; | |
| const sixes = keptDice.filter(v => v === 6).length; | |
| const volatilityTriggered = volatilityEnabled && (ones >= 2 || sixes >= 2); | |
| const flavor = volatilityTriggered | |
| ? `${type} <span title="Volatility triggered" style="color:#157a16"><i class="fas fa-bolt"></i></span>` | |
| : type; | |
| const message = await roll.toMessage({ | |
| flavor | |
| }); | |
| if (volatilityTriggered) { | |
| await new Promise(resolve => window.requestAnimationFrame(resolve)); | |
| const chatMessage = document.querySelector(`.message[data-message-id="${message.id}"]`); | |
| if (chatMessage) { | |
| chatMessage.style.border = "2px solid #67d068"; | |
| chatMessage.style.borderRadius = "8px"; | |
| chatMessage.style.color = "#157a16"; | |
| } | |
| } | |
| if (!volatilityTriggered) return; | |
| const volatility = await new Roll("1d6").evaluate(); | |
| await volatility.toMessage({ | |
| flavor: "Volatility" | |
| }); | |
| } | |
| new Dialog({ | |
| title: "Hazard Navigator", | |
| content: ` | |
| <form> | |
| <style> | |
| .statistic-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 8px; | |
| } | |
| .statistic-controls .volatility { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| white-space: nowrap; | |
| } | |
| .statistic-controls .modifier { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| flex: 1; | |
| } | |
| .statistic-controls .modifier input, | |
| .statistic-controls .stress select, | |
| .statistic-inputs input { | |
| width: 60px; | |
| text-align: center; | |
| } | |
| .statistic-inputs { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 0.5rem; | |
| margin-bottom: 8px; | |
| } | |
| .statistic-inputs label { | |
| display: flex; | |
| flex-direction: column; | |
| font-size: 0.9rem; | |
| align-items: center; | |
| } | |
| .statistic-roll-buttons { | |
| display: flex; | |
| gap: 4px; | |
| flex-wrap: nowrap; | |
| } | |
| .statistic-roll-buttons button { | |
| flex: 1 1 auto; | |
| min-width: 0; | |
| } | |
| </style> | |
| <div class="statistic-controls"> | |
| <label class="volatility"> | |
| <input id="volatility" type="checkbox" checked> | |
| Volatile? | |
| </label> | |
| <label class="modifier"> | |
| <span>Modifier</span> | |
| <input id="modifier" type="number" value="0"> | |
| </label> | |
| <label class="stress"> | |
| <span>Stress Level</span> | |
| <select id="stressLevel"> | |
| <option value="0" selected>0</option> | |
| <option value="-1">-1</option> | |
| <option value="-2">-2</option> | |
| <option value="-3">-3</option> | |
| <option value="-4">-4</option> | |
| <option value="-5">-5</option> | |
| </select> | |
| </label> | |
| </div> | |
| <div class="statistic-inputs"> | |
| <label> | |
| <span>Strength</span> | |
| <input id="stat-strength" type="number" min="1" max="12" value="${defaultStatistics.Strength}"> | |
| </label> | |
| <label> | |
| <span>Finesse</span> | |
| <input id="stat-finesse" type="number" min="1" max="12" value="${defaultStatistics.Finesse}"> | |
| </label> | |
| <label> | |
| <span>Intuition</span> | |
| <input id="stat-intuition" type="number" min="1" max="12" value="${defaultStatistics.Intuition}"> | |
| </label> | |
| <label> | |
| <span>Intellect</span> | |
| <input id="stat-intellect" type="number" min="1" max="12" value="${defaultStatistics.Intellect}"> | |
| </label> | |
| </div> | |
| <div class="statistic-controls"> | |
| <label class="modifier"> | |
| <span>Statistic</span> | |
| <select id="statisticSelect"> | |
| <option value="" selected></option> | |
| <option value="Strength">Strength</option> | |
| <option value="Finesse">Finesse</option> | |
| <option value="Intuition">Intuition</option> | |
| <option value="Intellect">Intellect</option> | |
| </select> | |
| </label> | |
| <label class="modifier"> | |
| <span>Skill</span> | |
| <select id="skillSelect"> | |
| <option value="" selected></option> | |
| </select> | |
| </label> | |
| </div> | |
| <div class="statistic-roll-buttons"> | |
| <button type="button" data-roll="+3">+3</button> | |
| <button type="button" data-roll="+2">+2</button> | |
| <button type="button" data-roll="+1">+1</button> | |
| <button type="button" data-roll="3d6">3d6</button> | |
| <button type="button" data-roll="-1">-1</button> | |
| <button type="button" data-roll="-2">-2</button> | |
| <button type="button" data-roll="-3">-3</button> | |
| </div> | |
| </form> | |
| `, | |
| buttons: {}, | |
| render: (html) => { | |
| html.find("[data-roll]").click(async ev => { | |
| const type = ev.currentTarget.dataset.roll; | |
| const modifier = Number(html.find("#modifier").val()) || 0; | |
| const volatilityEnabled = html.find("#volatility").is(":checked"); | |
| const stressLevel = Number(html.find("#stressLevel").val()) || 0; | |
| const statisticName = html.find("#statisticSelect").val() || ""; | |
| const statisticScore = statisticName | |
| ? Number(html.find(`#stat-${statisticName.toLowerCase()}`).val()) || 0 | |
| : 0; | |
| const selectedSkill = html.find("#skillSelect").val() || ""; | |
| const skillBonus = selectedSkill ? (characterSkills[selectedSkill] || 0) : 0; | |
| await makeRoll(type, modifier, volatilityEnabled, stressLevel, statisticName, statisticScore, skillBonus); | |
| // Keep window open for subsequent rolls | |
| // html.closest(".app").find(".close").click(); | |
| }); | |
| // Populate skill dropdown with skills that have level > 0 | |
| const skillSelect = html.find("#skillSelect"); | |
| Object.entries(characterSkills).forEach(([skillName, level]) => { | |
| if (level > 0) { | |
| skillSelect.append(`<option value="${skillName}">${skillName} (+${level})</option>`); | |
| } | |
| }); | |
| } | |
| }).render(true); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment