initial commit

This commit is contained in:
2025-06-07 07:58:30 -05:00
commit 55f791d177
14892 changed files with 1294507 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Dice Roller</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
touch-action: manipulation;
margin: 0;
}
.input-form {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: 10px;
width: 100%;
max-width: 300px;
}
.input-form label,
.input-form input {
flex: 1;
margin: 5px;
padding: 10px;
font-size: 1.2em;
width: 100%;
}
.input-form input {
text-align: center;
}
.dice-roller {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.dice {
margin: 10px;
padding: 20px;
font-size: 1.5em;
cursor: pointer;
}
.history {
width: 90%;
max-width: 400px;
margin: 20px 0;
padding: 10px;
border: 1px solid #000;
background-color: #fff;
max-height: 300px;
overflow-y: auto;
font-size: 1.5em;
}
.clear-history {
margin-top: 10px;
font-size: 1.2em;
padding: 10px;
font-weight: normal;
width: 100%;
}
p {
text-align: center;
width: 95%;
max-width: 800px;
font-size: 1.2em;
}
.custom-roll {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-top: 10px;
}
.custom-roll input,
.custom-roll button {
margin: 5px;
padding: 10px;
font-size: 1.2em;
width: 100%;
}
div.c1 {
display: flex;
flex-direction: row;
align-items: center;
}
</style>
</head>
<body>
<h1>Dice Roller</h1>
<form class="input-form">
<div class="c1">
<label for="dice">Dice</label>
<input type="number" id="dice" name="dice" value="1">
</div>
<div class="c1">
<label for="modifier">Mod</label>
<input type="number" id="modifier" name="modifier" value="0">
</div>
</form>
<div class="dice-roller">
<button class="dice" data-sides="4">d4</button>
<button class="dice" data-sides="6">d6</button>
<button class="dice" data-sides="8">d8</button>
<button class="dice" data-sides="10">d10</button>
<button class="dice" data-sides="12">d12</button>
<button class="dice" data-sides="20">d20</button>
<button class="dice" data-sides="100">d100</button>
</div>
<div class="custom-roll">
<input type="number" id="custom-sides" name="custom-sides" value="3" min="2">
<button type="button" class="roll-custom-dice">Custom</button>
</div>
<form class="input-form">
<button type="button" class="clear-history">Clear History</button>
</form>
<div class="history" id="history"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const diceButtons = document.querySelectorAll('.dice');
const history = document.getElementById('history');
const diceForm = document.querySelector('.input-form');
const clearHistoryBtn = document.querySelector('.clear-history');
const rollCustomDiceBtn = document.querySelector('.roll-custom-dice');
const customSidesInput = document.getElementById('custom-sides');
const diceSidesInput = document.querySelector('[name="dice"]');
diceButtons.forEach(button => {
button.addEventListener('click', () => {
const sides = parseInt(button.dataset.sides);
let rolls = diceForm.elements.dice.value;
let modifier = diceForm.elements.modifier.value;
rolls = parseInt(rolls) || 1;
modifier = parseInt(modifier) || 0;
let rollsArray = [];
let total = 0;
for (let i = 0; i < rolls; i++) {
let roll = Math.floor(Math.random() * sides) + 1;
rollsArray.push(roll);
total += roll;
}
total += modifier;
let rollStr = `${rolls}d${sides}`;
if (rolls > 1) {
rollStr += ` (${rollsArray.join(', ')})`;
}
if (modifier !== 0) {
rollStr += ` + ${modifier}`;
}
rollStr += ` = ${total}`;
rollStr += `<br>`;
history.innerHTML = rollStr + history.innerHTML;
history.scrollTop = 0;
});
});
clearHistoryBtn.addEventListener('click', () => {
history.innerHTML = '';
});
rollCustomDiceBtn.addEventListener('click', () => {
let rolls = parseInt(diceForm.elements.dice.value) || 1;
let modifier = parseInt(diceForm.elements.modifier.value) || 0;
let sides = parseInt(customSidesInput.value) || 10;
let rollsArray = [];
let total = 0;
for (let i = 0; i < rolls; i++) {
let roll = Math.floor(Math.random() * sides) + 1;
rollsArray.push(roll);
total += roll;
}
total += modifier;
let rollStr = `${rolls}d${sides}`;
if (rolls > 1) {
rollStr += ` (${rollsArray.join(', ')})`;
}
if (modifier !== 0) {
rollStr += ` + ${modifier}`;
}
rollStr += ` = ${total}`;
if (rolls > 1) {
rollStr += ` (${total - modifier})`;
}
rollStr += `<br>`;
history.innerHTML = rollStr + history.innerHTML;
history.scrollTop = 0;
});
});
</script>
<p>This tool is released under a <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0 Universal</a> license. You can copy, modify, and distribute this tool, even for commercial purposes, all without asking permission.</p>
</body>
</html>

View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html>
<head>
<title>Forge of Foes 5e Monster Stats</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
font-size: 18px;
}
#container {
max-width: 700px;
margin: auto;
}
#statBlock {
border: 1px solid #ccc;
padding: 25px;
margin-top: 15px;
text-align: left;
line-height: 1.6;
font-size: 24px;
}
#crTitle {
font-size: 32px;
margin-bottom: 15px;
}
#crSelect {
font-size: 24px;
padding: 10px;
width: 60%;
}
.license {
font-size: 12px
}
h1 {text-align: center}
</style>
</head>
<body>
<div id="container">
<h1><em><a href="https://shop.slyflourish.com/collections/the-lazy-dungeon-master-series/products/forge-of-foes">Forge of Foes</a></em> 5e Monster Stats</h1>
<label for="crSelect">Select CR:</label>
<select id="crSelect">
<!-- CR Options -->
</select>
<div id="statBlock">
<!-- Stat Block -->
</div>
<h3>Monster Features</h3>
<p>Give any custom monster impactful features and attacks that make sense for their place in the game. When a monster feature deals damage, choose a damage type appropriate to the creature's physiology, theme, or story. A creature channeling magical power might deal acid, cold, fire, lightning, force, poison, psychic, necrotic, radiant, or thunder damage. A creature making use of spines, spikes, or projectiles might deal bludgeoning, piercing, or slashing damage.</p>
<p><strong><em>Damaging Blast.</em></strong> This creature has one or more single-target ranged attacks using the attack bonus and damage calculated above, and which deal damage of an appropriate type.</p>
<p><strong><em>Damage Reflection.</em></strong> Whenever a creature within 5 feet of this creature hits them with a melee attack, the attacker takes damage in return of a type appropriate to the creature. The damage dealt is equal to half the damage of one of this creature's attacks. If you give a creature this feature, give them one less attack than normal.</p>
<p><strong><em>Misty Step.</em></strong> As a bonus action, this creature can teleport up to 30 feet to an unoccupied space they can see.</p>
<p><strong><em>Knockdown.</em></strong> When this creature hits a target with a melee attack, the target must succeed on a Strength saving throw or be knocked prone.</p>
<p><strong><em>Restraining Grab.</em></strong> When this creature hits a target with a melee attack, the target is grappled (escape DC based on this creature's Strength or Dexterity modifier). While grappled, the target is restrained.</p>
<p><strong><em>Damaging Burst.</em></strong> As an action, this creature can create a burst of energy, magic, spines, or some other effect in a 10-foot-radius sphere, either around themself or at a point within 120 feet. Each creature in that area must make a Dexterity, Constitution, or Wisdom saving throw (your choice, based on the type of burst). On a failure, a target takes damage of an appropriate type equal to half this creature's total damage per round. On a success, a target takes half as much damage.</p>
<p><strong><em>Cunning Action</em></strong>. On each of their turns, this creature can use a bonus action to take the Dash, Disengage, or Hide action.</p>
<p><strong><em>Damaging Aura.</em></strong> Each creature who starts their turn within 10 feet of this creature takes damage of a type appropriate to the creature. The damage dealt is equal to half the damage of one of this creature's attacks. If you give a creature this feature, give them one less attack than normal.</p>
<p><strong><em>Energy Weapons.</em></strong> The creature's weapon attacks deal extra damage of an appropriate type. You can add this damage on top of the creature's regular damage output to give them a combat boost, or you can replace some of the creature's normal weapon damage with this energy damage.</p>
<p><strong><em>Damage Transference.</em></strong> When this creature takes damage, they can transfer half or all of that damage (your choice) to a willing creature within 30 or 60 feet of them. This feature is particularly good for boss monsters.</p>
<h2>Conditions</h2>
<p>Creatures may inflict conditions on attacks. Here's a list of potential conditions. Be careful to ensure such conditions don't take away too much agency from the characters or their players. Aim towards fun.</p>
<h3>Blinded</h3>
<ul>
<li>A blinded creature can't see and it automatically fails ability checks that require sight.</li>
<li>Attack rolls against a blinded creature are made with advantage, and the creature's attack rolls are made with disadvantage.</li>
</ul>
<h3>Charmed</h3>
<ul>
<li>A charmed creature can't take any hostile action against the charmer.</li>
<li>Ability checks the charmer makes to socially interact with the charmed creature have advantage.</li>
</ul>
<h3>Confused</h3>
<ul>
<li>A confused creature can't take reactions.</li>
<li>On its turn a confused creature rolls a d8 to determine what it does.
<ul>
<li>On a 1 to 4, a confused creature does nothing. </li>
<li>On a 5 or 6, a confused creature takes no action or bonus action and uses all its movement to move in a randomly determined direction. </li>
<li>On a 7 or 8, a confused creature makes a melee attack against a randomly determined creature within its reach or does nothing if it can't make such an attack.</li>
</ul>
</li>
</ul>
<h3>Doomed</h3>
<ul>
<li>A doomed creature dies at a time determined by the GM, or within 13 (2d12) hours.</li>
<li>A doomed creature continues to be doomed even after it dies. Magic equivalent to a 7th-level or higher spell can remove the doomed condition (such as regenerate cast on a living creature, resurrection, true resurrection, or wish).</li>
</ul>
<h3>Frightened</h3>
<ul>
<li>A frightened creature has disadvantage on ability checks and attack rolls while it is able to see the source of its fear.</li>
<li>A frightened creature can't willingly move closer to the source of its fear.</li>
</ul>
<h3>Grappled</h3>
<ul>
<li>A grappled creature's Speed becomes 0, and it can't benefit from bonuses to movement speeds.</li>
<li>If the grappler becomes incapacitated the condition ends.</li>
<li>If an effect removes the grappled creature from the reach of the grappler or grappling effect the condition ends.</li>
</ul>
<h3>Incapacitated</h3>
<ul>
<li>An incapacitated creature can't take actions, bonus actions, or reactions.</li>
</ul>
<h3>Paralyzed</h3>
<ul>
<li>A paralyzed creature is incapacitated and can't move or speak.</li>
<li>A paralyzed creature automatically fails Strength and Dexterity saving throws. </li>
<li>Attack rolls against a paralyzed creature have advantage.</li>
<li>Any attack that hits a paralyzed creature is a critical hit if the attacker is within 5 feet.</li>
</ul>
<h3>Petrified</h3>
<ul>
<li>A petrified creature (and all of its mundane possessions) is transformed into a solid inanimate substance (usually stone). </li>
<li>A petrified creature's weight is increased by a factor of ten and it ceases aging.</li>
<li>A petrified creature is incapacitated, can't move or speak, and is unaware of its surroundings.</li>
<li>A petrified creature automatically fails Strength and Dexterity saving throws.</li>
<li>A petrified creature has resistance to all damage.</li>
<li>A petrified creature is immune to poison and disease (time spent petrified does not affect the duration of a poison or disease already in its system).</li>
</ul>
<h3>Poisoned</h3>
<ul>
<li>A poisoned creature has disadvantage on attack rolls and ability checks.</li>
</ul>
<h3>Prone</h3>
<ul>
<li>A prone creature's only movement option is to crawl (every 1 foot of movement while crawling costs 1 extra foot) until it stands up.</li>
<li>Standing up requires half a creature's movement.</li>
<li>A prone creature makes melee attack rolls with disadvantage.</li>
<li>An attack roll against a prone creature is made with advantage if the attacker is within 5 feet. Otherwise, the attack roll is made with disadvantage.</li>
</ul>
<h3>Rattled</h3>
<ul>
<li>A rattled creature cannot take reactions.</li>
<li>A creature that is immune to being stunned is immune to being rattled.</li>
</ul>
<h3>Restrained</h3>
<ul>
<li>A restrained creature's Speed becomes 0, and it can't benefit from bonuses to speed.</li>
<li>Attack rolls against a restrained creature are made with advantage.</li>
<li>A restrained creature makes attack rolls with disadvantage.</li>
<li>The restrained creature has disadvantage on Dexterity saving throws.</li>
</ul>
<h3>Slowed</h3>
<ul>
<li>A slowed creature's Speed is halved.</li>
<li>A slowed creature takes a 2 penalty to AC and Dexterity saving throws.</li>
<li>A slowed creature cannot take reactions.</li>
<li>On its turn, a slowed creature can take either an action or a bonus action, not both. In addition, it can't make more than one melee or ranged attack during its turn.</li>
</ul>
<h3>Stunned</h3>
<ul>
<li>A stunned creature is incapacitated (see the condition), can't move, and can speak only falteringly.</li>
<li>The creature automatically fails Strength and Dexterity saving throws.</li>
<li>Attack rolls against the creature have advantage.</li>
</ul>
<h3>Unconscious</h3>
<ul>
<li>An unconscious creature is incapacitated, can't move or speak, and is unaware of its surroundings.</li>
<li>An unconscious creature drops whatever it's holding and falls prone.</li>
<li>An unconscious creature automatically fails Strength and Dexterity saving throws.</li>
<li>Attack rolls against an unconscious creature are made with advantage.</li>
<li>Any attack that hits an unconscious creature is a critical hit if the attacker is within 5 feet.</li>
</ul>
<h3>Tracked Conditions</h3>
<p>Various challenges, obstacles, and magics can lead to either fatigue or strife. An effect can give a creature one or more levels of fatigue or strife (detailed in the effect's description).</p>
<p>If a creature suffering from fatigue or strife fails to resist another effect that causes a level of the tracked condition, its current level increases by the amount specified in the effect's description.</p>
<p>A creature suffers the effect of its current level in a tracked condition as well as all lower levels. For example, a creature suffering level 3 fatigue has its Speed halved, it cannot Sprint, and it makes Strength, Dexterity, and Constitution checks with disadvantage.</p>
<p>An effect that removes a tracked condition reduces its level as specified in the effect's description, with all tracked condition effects ending when a creature's condition level is reduced below 1.</p>
<p>Finishing a long rest reduces a creature's fatigue and strife levels by 1.</p>
<h3>Fatigue</h3>
<p>Fatigue represents exhaustion, exposure, hunger, injuries, and other physical factors which gradually wear a creature down. A creature which reaches the 7th level of the fatigue track becomes doomed and dies.</p>
<p><strong>Fatigue Level Effects</strong></p>
<ol>
<li>Cannot dash.</li>
<li>Disadvantage on Strength, Dexterity, and Constitution checks.</li>
<li>Speed halved.</li>
<li>Disadvantage on attack rolls and saving throws using Strength, Dexterity, or Constitution.</li>
<li>Hit Dice halved.</li>
<li>Speed reduced to 5 ft.</li>
<li>Doomed.</li>
</ol>
<h3>Strife</h3>
<p>Strife represents corruption, despair, fear, loss of resolve, and other mental factors which gradually undo a creature's very soul. A creature which reaches the 7th level of the strife track suffers a special, permanent effect, which is either randomly selected or decided by the GM. This might involve the creature shutting down completely, or being impacted in such a way that it is forever changed.</p>
<p><strong>Strife Level Effects</strong></p>
<ol>
<li>Disadvantage on Intelligence, Wisdom, and Charisma checks.</li>
<li>Disadvantage on concentration checks.</li>
<li>Can only take a bonus action or action each turn (not both).</li>
<li>Disadvantage on attack rolls and saving throws using Intelligence, Wisdom, and Charisma.</li>
<li>Suffer the effects of a randomly determined short-term mental stress effect.</li>
<li>Cannot cast spells (but can cast cantrips).</li>
<li>Suffer the effects of a randomly determined long-term mental stress effect.</li>
</ol>
<p class="license">This work includes material taken from the <a href="https://slyflourish.com/lazy_5e_monster_building_resource_document.html">Lazy GM's 5e Monster Builder Resource Document</a> written by Teos Abadía of <a href="https://alphastream.org">Alphastream.org</a>, Scott Fitzgerald Gray of <a href="https://insaneangel.com">Insaneangel.com</a>, and Michael E. Shea of <a href="https://slyflourish.com">SlyFlourish.com</a>, available under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</p>
<p class="license">This work includes material taken from the <a href="https://a5esrd.com/a5esrd">A5E System Reference Document (A5ESRD)</a> by EN Publishing and available at A5ESRD.com, based on Level Up: Advanced 5th Edition, available at www.levelup5e.com. The A5ESRD is licensed under the Creative Commons Attribution 4.0 International License available at <a href="https://creativecommons.org/licenses/by/4.0/legalcode">https://creativecommons.org/licenses/by/4.0/legalcode</a>.</p>
<p class="license">This work includes material taken from the System Reference Document 5.1 ("SRD 5.1") by Wizards of the Coast LLC and available at <a href="https://dnd.wizards.com/resources/systems-reference-document">https://dnd.wizards.com/resources/systems-reference-document</a>. The SRD 5.1 is licensed under the Creative Commons Attribution 4.0 International License available at <a href="https://creativecommons.org/licenses/by/4.0/legalcode">https://creativecommons.org/licenses/by/4.0/legalcode</a>.</p>
</div>
<script>
const data = {
"CR 0": {"AC/DC": 10, "HP": "3 (2-4)", "Atk/Prof": "+2", "Damage Per Round": "2", "# Atks": "1", "Dmg": "2 (1d4)","Example Monsters": "Commoner, rat, spider"},
"CR 1/8": {"AC/DC": 11, "HP": "9 (7-11)", "Atk/Prof": "+3", "Damage Per Round": "3", "# Atks": "1", "Dmg": "4 (1d6 + 1)","Example Monsters": "Bandit, cultist, giant rat"},
"CR 1/4": {"AC/DC": 11, "HP": "13 (10-16)", "Atk/Prof": "+3", "Damage Per Round": "5", "# Atks": "1", "Dmg": "5 (1d6 + 2)","Example Monsters": "Acolyte, skeleton, wolf"},
"CR 1/2": {"AC/DC": 12, "HP": "22 (17-28)", "Atk/Prof": "+4", "Damage Per Round": "8", "# Atks": "2", "Dmg": "4 (1d4 + 2)","Example Monsters": "Black bear, scout, shadow"},
"CR 1": {"AC/DC": 12, "HP": "33 (25-41)", "Atk/Prof": "+5", "Damage Per Round": "12", "# Atks": "2", "Dmg": "6 (1d8 + 2)","Example Monsters": "Dire wolf, specter, spy"},
"CR 2": {"AC/DC": 13, "HP": "45 (34-56)", "Atk/Prof": "+5", "Damage Per Round": "17", "# Atks": "2", "Dmg": "9 (2d6 + 2)","Example Monsters": "Ghast, ogre, priest"},
"CR 3": {"AC/DC": 13, "HP": "65 (49-81)", "Atk/Prof": "+5", "Damage Per Round": "23", "# Atks": "2", "Dmg": "12 (2d8 + 3)","Example Monsters": "Knight, mummy, werewolf"},
"CR 4": {"AC/DC": 14, "HP": "84 (64-106)", "Atk/Prof": "+6", "Damage Per Round": "28", "# Atks": "2", "Dmg": "14 (3d8 + 1)","Example Monsters": "Ettin, ghost"},
"CR 5": {"AC/DC": 15, "HP": "95 (71-119)", "Atk/Prof": "+7", "Damage Per Round": "35", "# Atks": "3", "Dmg": "12 (3d6 + 2)","Example Monsters": "Elemental, gladiator, vampire spawn"},
"CR 6": {"AC/DC": 15, "HP": "112 (84-140)", "Atk/Prof": "+7", "Damage Per Round": "41", "# Atks": "3", "Dmg": "14 (3d6 + 4)","Example Monsters": "Mage, medusa, wyvern"},
"CR 7": {"AC/DC": 15, "HP": "130 (98-162)", "Atk/Prof": "+7", "Damage Per Round": "47", "# Atks": "3", "Dmg": "16 (3d8 + 3)","Example Monsters": "Stone giant, young black dragon"},
"CR 8": {"AC/DC": 15, "HP": "136 (102-170)", "Atk/Prof": "+7", "Damage Per Round": "53", "# Atks": "3", "Dmg": "18 (3d10 + 2)","Example Monsters": "Assassin, frost giant"},
"CR 9": {"AC/DC": 16, "HP": "145 (109-181)", "Atk/Prof": "+8", "Damage Per Round": "59", "# Atks": "3", "Dmg": "19 (3d10 + 3)","Example Monsters": "Bone devil, fire giant, young blue dragon"},
"CR 10": {"AC/DC": 17, "HP": "155 (116-194)", "Atk/Prof": "+9", "Damage Per Round": "65", "# Atks": "4", "Dmg": "16 (3d8 + 3)","Example Monsters": "Stone golem, young red dragon"},
"CR 11": {"AC/DC": 17, "HP": "165 (124-206)", "Atk/Prof": "+9", "Damage Per Round": "71", "# Atks": "4", "Dmg": "18 (3d10 + 2)","Example Monsters": "Djinni, efreeti, horned devil"},
"CR 12": {"AC/DC": 17, "HP": "175 (131-219)", "Atk/Prof": "+9", "Damage Per Round": "77", "# Atks": "4", "Dmg": "19 (3d10 + 3)","Example Monsters": "Archmage, erinyes"},
"CR 13": {"AC/DC": 18, "HP": "184 (138-230)", "Atk/Prof": "+10", "Damage Per Round": "83", "# Atks": "4", "Dmg": "21 (4d8 + 3)","Example Monsters": "Adult white dragon, storm giant, vampire"},
"CR 14": {"AC/DC": 19, "HP": "196 (147-245)", "Atk/Prof": "+11", "Damage Per Round": "89", "# Atks": "4", "Dmg": "22 (4d10)","Example Monsters": "Adult black dragon, ice devil"},
"CR 15": {"AC/DC": 19, "HP": "210 (158-263)", "Atk/Prof": "+11", "Damage Per Round": "95", "# Atks": "5", "Dmg": "19 (3d10 + 3)","Example Monsters": "Adult green dragon, mummy lord, purple worm"},
"CR 16": {"AC/DC": 19, "HP": "229 (172-286)", "Atk/Prof": "+11", "Damage Per Round": "101", "# Atks": "5", "Dmg": "21 (4d8 + 3)","Example Monsters": "Adult blue dragon, iron golem, marilith"},
"CR 17": {"AC/DC": 20, "HP": "246 (185-308)", "Atk/Prof": "+12", "Damage Per Round": "107", "# Atks": "5", "Dmg": "22 (3d12 + 3)","Example Monsters": "Adult red dragon"},
"CR 18": {"AC/DC": 21, "HP": "266 (200-333)", "Atk/Prof": "+13", "Damage Per Round": "113", "# Atks": "5", "Dmg": "23 (4d10 + 1)","Example Monsters": "Demilich"},
"CR 19": {"AC/DC": 21, "HP": "285 (214-356)", "Atk/Prof": "+13", "Damage Per Round": "119", "# Atks": "5", "Dmg": "24 (4d10 + 2)","Example Monsters": "Balor"},
"CR 20": {"AC/DC": 21, "HP": "300 (225-375)", "Atk/Prof": "+13", "Damage Per Round": "132", "# Atks": "5", "Dmg": "26 (4d12)","Example Monsters": "Ancient white dragon, pit fiend"},
"CR 21": {"AC/DC": 22, "HP": "325 (244-406)", "Atk/Prof": "+14", "Damage Per Round": "150", "# Atks": "5", "Dmg": "30 (4d12 + 4)","Example Monsters": "Ancient black dragon, lich, solar"},
"CR 22": {"AC/DC": 23, "HP": "350 (263-438)", "Atk/Prof": "+15", "Damage Per Round": "168", "# Atks": "5", "Dmg": "34 (4d12 + 8)","Example Monsters": "Ancient green dragon"},
"CR 23": {"AC/DC": 23, "HP": "375 (281-469)", "Atk/Prof": "+15", "Damage Per Round": "186", "# Atks": "5", "Dmg": "37 (6d10 + 4)","Example Monsters": "Ancient blue dragon, kraken"},
"CR 24": {"AC/DC": 23, "HP": "400 (300-500)", "Atk/Prof": "+15", "Damage Per Round": "204", "# Atks": "5", "Dmg": "41 (6d10 + 8)","Example Monsters": "Ancient red dragon"},
"CR 25": {"AC/DC": 24, "HP": "430 (323-538)", "Atk/Prof": "+16", "Damage Per Round": "222", "# Atks": "5", "Dmg": "44 (6d10 + 11)","Example Monsters": "Demon princes, archdevils"},
"CR 26": {"AC/DC": 25, "HP": "460 (345-575)", "Atk/Prof": "+17", "Damage Per Round": "240", "# Atks": "5", "Dmg": "48 (6d10 + 15)","Example Monsters": "Demon princes, archdevils"},
"CR 27": {"AC/DC": 25, "HP": "490 (368-613)", "Atk/Prof": "+17", "Damage Per Round": "258", "# Atks": "5", "Dmg": "52 (6d10 + 19)","Example Monsters": "Demon princes, archdevils"},
"CR 28": {"AC/DC": 25, "HP": "540 (405-675)", "Atk/Prof": "+17", "Damage Per Round": "276", "# Atks": "5", "Dmg": "55 (6d10 + 22)","Example Monsters": "Demon princes, archdevils"},
"CR 29": {"AC/DC": 26, "HP": "600 (450-750)", "Atk/Prof": "+18", "Damage Per Round": "294", "# Atks": "5", "Dmg": "59 (6d10 + 26)","Example Monsters": "Demon princes, archdevils"},
"CR 30": {"AC/DC": 27, "HP": "666 (500-833)", "Atk/Prof": "+19", "Damage Per Round": "312", "# Atks": "5", "Dmg": "62 (6d10 + 29)","Example Monsters": "Demigods, tarrasque"},
};
// Populate CR options
const crSelect = document.getElementById('crSelect');
for (const cr in data) {
const option = document.createElement("option");
option.value = cr;
option.text = cr;
crSelect.appendChild(option);
}
function getCRFromURL() {
const params = new URLSearchParams(window.location.search);
let cr = params.get('cr');
if (!cr) return "CR 0"; // Default value
return `CR ${cr}`;
}
function setCRToURL(cr) {
const url = new URL(window.location);
url.searchParams.set('cr', cr.split(" ")[1]); // Get just the number part
history.pushState({}, '', url);
}
function showStats() {
const cr = crSelect.value;
const stats = data[cr];
let output = `<div id='crTitle'>${cr}</div>`;
for (const key in stats) {
output += `<strong>${key}:</strong> ${stats[key]}<br>`;
}
// Add single line stat block
let singleLineStatBlock = `${cr} AC/DC ${stats["AC/DC"]} HP ${stats["HP"]} Atk/Prof ${stats["Atk/Prof"]} DPR ${stats["Damage Per Round"]} Atks ${stats["# Atks"]} × ${stats["Dmg"]}`;
output += `<div><strong>One Line Stat Block:</strong> ${singleLineStatBlock}</div>`;
document.getElementById('statBlock').innerHTML = output;
}
crSelect.addEventListener('change', function() {
showStats();
setCRToURL(crSelect.value);
});
// Set initial value from URL and show stats
crSelect.value = getCRFromURL();
showStats();
</script>
</body>
</html>

View File

@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="user-scalable=yes, width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<meta charset="UTF-8">
<style>
body {
max-width: 700px;
margin: auto;
font-size: 18px;
font-family: sans-serif;
padding: 10px;
}
p, li {
line-height: 1.5em;
}
ul, ol {
padding-left: 20px;
margin-left: 0;
}
li {
margin-left: 0;
}
table {
border-collapse: collapse;
}
th, td {
border: 1px solid black;
padding: 5px;
text-align: left;
}
canvas {
max-width: 100%;
height: auto;
display: block;
background: #ccc;
margin-top: 1rem;
border-radius: 50%;
cursor: grab;
touch-action: none;
}
input[type="color"] {
width: 50px;
height: 30px;
padding: 0;
border: none;
vertical-align: middle;
}
button, #fileButton {
font-size: 18px;
padding: 10px 16px;
border: none;
background-color: #d3d3d3;
color: #000;
border-radius: 6px;
cursor: pointer;
display: inline-block;
user-select: none;
-webkit-tap-highlight-color: transparent;
width: auto;
min-width: 140px;
text-align: center;
margin: 0.5rem 0.5rem 0.5rem 0;
}
button:active, #fileButton:active {
background-color: #a9a9a9;
}
#fileButton {
position: relative;
overflow: hidden;
}
#fileButton input[type="file"] {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
opacity: 0;
cursor: pointer;
}
#ringColorLabel {
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
margin: 1rem 0;
}
#ringColor {
flex-shrink: 0;
cursor: pointer;
}
</style>
<title>VTT Token Generator</title>
</head>
<body>
<h1>VTT Token Generator</h1>
<p>Upload an image, use your mouse wheel or pinchies to zoom in and out, and download a PNG token for your VTT.</p>
<label id="fileButton" role="button" aria-label="Choose File" tabindex="0">
Choose File
<input type="file" id="imageInput" accept="image/*" />
</label>
<div id="ringColorLabel">
<input type="color" id="ringColor" value="#333333" />
<span>Ring Color</span>
</div>
<button id="downloadBtn" aria-label="Generate Token" type="button">Generate Token</button>
<p><canvas id="tokenCanvas" width="512" height="512"></canvas></p>
<script>
const imageInput = document.getElementById('imageInput');
const ringColor = document.getElementById('ringColor');
const canvas = document.getElementById('tokenCanvas');
const ctx = canvas.getContext('2d');
const downloadBtn = document.getElementById('downloadBtn');
let uploadedImage = null;
let imgX = 0, imgY = 0;
let imgScale = 1;
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let lastDist = null;
let touchDragging = false;
let touchStartX = 0, touchStartY = 0;
let touchImgX = 0, touchImgY = 0;
function resetView() {
imgScale = Math.min(canvas.width / uploadedImage.width, canvas.height / uploadedImage.height);
imgX = (canvas.width - uploadedImage.width * imgScale) / 2;
imgY = (canvas.height - uploadedImage.height * imgScale) / 2;
}
function render() {
const size = canvas.width;
ctx.clearRect(0, 0, size, size);
ctx.save();
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2 - 10, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
if (uploadedImage) {
ctx.drawImage(uploadedImage, imgX, imgY, uploadedImage.width * imgScale, uploadedImage.height * imgScale);
}
ctx.restore();
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2 - 5, 0, Math.PI * 2);
ctx.strokeStyle = ringColor.value;
ctx.lineWidth = 10;
ctx.stroke();
}
imageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
uploadedImage = img;
resetView();
render();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
ringColor.addEventListener('input', () => {
if (!uploadedImage) return;
render();
});
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
dragStartX = e.offsetX - imgX;
dragStartY = e.offsetY - imgY;
canvas.style.cursor = 'grabbing';
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
canvas.style.cursor = 'grab';
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
canvas.style.cursor = 'grab';
});
canvas.addEventListener('mousemove', (e) => {
if (isDragging && uploadedImage) {
imgX = e.offsetX - dragStartX;
imgY = e.offsetY - dragStartY;
render();
}
});
canvas.addEventListener('wheel', (e) => {
if (!uploadedImage) return;
e.preventDefault();
const scaleAmount = -e.deltaY * 0.001;
const prevScale = imgScale;
imgScale *= (1 + scaleAmount);
imgScale = Math.max(0.1, Math.min(10, imgScale));
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const dx = mx - imgX;
const dy = my - imgY;
imgX -= dx * (imgScale / prevScale - 1);
imgY -= dy * (imgScale / prevScale - 1);
render();
}, { passive: false });
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
lastDist = getDistance(e.touches[0], e.touches[1]);
} else if (e.touches.length === 1) {
touchDragging = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchImgX = imgX;
touchImgY = imgY;
}
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
if (e.touches.length === 2 && uploadedImage) {
e.preventDefault();
const newDist = getDistance(e.touches[0], e.touches[1]);
const scaleChange = newDist / lastDist;
const prevScale = imgScale;
imgScale *= scaleChange;
imgScale = Math.max(0.1, Math.min(10, imgScale));
const rect = canvas.getBoundingClientRect();
const mx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
const my = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
const dx = mx - imgX;
const dy = my - imgY;
imgX -= dx * (imgScale / prevScale - 1);
imgY -= dy * (imgScale / prevScale - 1);
lastDist = newDist;
render();
} else if (touchDragging && e.touches.length === 1 && uploadedImage) {
e.preventDefault();
const dx = e.touches[0].clientX - touchStartX;
const dy = e.touches[0].clientY - touchStartY;
imgX = touchImgX + dx;
imgY = touchImgY + dy;
render();
}
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
if (e.touches.length === 0) {
touchDragging = false;
lastDist = null;
}
}, { passive: false });
function getDistance(touch1, touch2) {
const dx = touch2.clientX - touch1.clientX;
const dy = touch2.clientY - touch1.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
downloadBtn.addEventListener('click', () => {
if (!uploadedImage) {
alert('Upload an image first.');
return;
}
render();
const dataUrl = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'token.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
</script>
<p>This tool is released under a <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0 Universal</a> license. You can copy, modify, and distribute this tool, even for commercial purposes, all without asking permission.</p>
<p>No images are saved to any server when using this tool. All file manipulation happens in your local web browser.</p>
</body>
</html>