Skip to content

Instantly share code, notes, and snippets.

@revilowaldow
Last active June 7, 2026 15:49
Show Gist options
  • Select an option

  • Save revilowaldow/3f9cb91efd0bc31cdb4eaeb67fa7b0d4 to your computer and use it in GitHub Desktop.

Select an option

Save revilowaldow/3f9cb91efd0bc31cdb4eaeb67fa7b0d4 to your computer and use it in GitHub Desktop.
HAZARD NAVIGATOR
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