//if (MarkStart) {MarkStart('DifficultyRating');} /* * Difficulty Rating - 5e Encounter Calculator * by Michael Greene (Volt Cruelerz) * */ on('ready', () => { const drname = 'Difficulty Rating'; // Initialize the state const ConfigureState = () => { const version = 1.04; if (!state.DifficultyRating) { state.DifficultyRating = { Party: [], NPCChallengeField: 'npc_challenge', IsNPCField: 'npc', Sheet: 'STARTUP', Version: version }; } if (!state.DifficultyRating.Version || state.DifficultyRating.Version < 1.04) { log('Upgrading Difficulty Rating to ' + version); state.DifficultyRating.Sheet = 'STARTUP'; state.DifficultyRating.Version = version; } }; ConfigureState(); // Check for initial startup if (state.DifficultyRating.Sheet === 'STARTUP') { log('Startup of Difficulty Rating. Prompting user for mode configuration.'); sendChat(drname, `/w gm [Set Difficulty Rating Mode](!dr --setMode ?{Select a mode|OGL|5e-Shaped})`); } const getCharByAny = (nameOrId) => { let character = null; // Try to directly load the character ID character = getObj('character', nameOrId); if (character) { return character; } // Try to load indirectly from the token ID const token = getObj('graphic', nameOrId); if (token) { character = getObj('character', token.get('represents')); if (character) { return character; } } // Try loading through char name const list = findObjs({ _type: 'character', name: nameOrId, }); if (list.length === 1) { return list[0]; } // Default to null return null; }; const getAttrs = (char, attrName) => { const attr = filterObjs((obj) => { if (obj.get('type') === 'attribute' && obj.get('characterid') === char.id && obj.get('name') == attrName) { return obj; } }); if (!attr || attr.length === 0) { log('No Attr: ' + char + ': ' + attrName); return null; } return attr; }; const getAttr = (char, attrName) => { let attr = getAttrs(char, attrName); if (!attr) { return null; } return attr[0]; }; const getAttrsFromSub = (char, substringName) => { const attr = filterObjs((obj) => { if (obj.get('type') === 'attribute' && obj.get('characterid') === char.id && obj.get('name').indexOf(substringName) !== -1) { return obj; } }); if (!attr || attr.length === 0) { log('No Substr Attr: ' + char + ': ' + substringName); return null; } return attr; }; const getAttrFromSub = (char, substringName) => { return getAttrsFromSub(char, substringName)[0]; }; // Pulls the interior message out of carets (^) const Decaret = (quotedString) => { const startQuote = quotedString.indexOf('^'); const endQuote = quotedString.lastIndexOf('^'); if (startQuote >= endQuote) { if (!quietMode) { sendChat(drname, `**ERROR:** You must have a string within carets in the phrase ${string}`); } return null; } return quotedString.substring(startQuote + 1, endQuote); }; class MonsterType { constructor(name, cr) { this.Name = name; this.CR = cr; this.Count = 1; } } const Ratings = { Trivial: `This encounter should pose no threat to the party, but may delay them if they opt to not expend resources.`, Easy: `An easy encounter doesn't tax the characters' resources or put them in serious peril. They might lose a few hit points, but victory is pretty much guaranteed.`, Medium: `A medium encounter usually has one or two scary moments for the players, but the characters should emerge victorius with no casualties. One or more of them might need to use healing resources.`, Hard: `A hard encounter could go badly for the adventurers. Weaker characters might get taken out of the fight, and there's a slim chance that one or more characters might die.`, Deadly: `A deadly encounter could be lethal for one or more player characters. Survival often requires good tactics and quick thinking, and the party risks defeat.` }; // Converts level-1 to [easy, medium, hard, deadly] exp thresholds const XPThresholds = [ [25, 50, 75, 100], [50, 100, 150, 200], [75, 150, 225, 400], [125, 250, 375, 500], [250, 500, 750, 1100], [300, 600, 900, 1400], [350, 750, 1100, 1700], [450, 900, 1400, 2100], [550, 1100, 1600, 2400], [600, 1200, 1900, 2800], [800, 1600, 2400, 3600], [1000, 2000, 3000, 4500], [1100, 2200, 3400, 5100], [1250, 2500, 3800, 5700], [1400, 2800, 4300, 6400], [1600, 3200, 4800, 7200], [2000, 3900, 5900, 8800], [2100, 4200, 6300, 9500], [2400, 4900, 7300, 10900], [2800, 5700, 8500, 12700] ]; const CRToExp = {}; const BuildCRToExp = () => { CRToExp[0] = 10; CRToExp[1/8] = 25; CRToExp[1/4] = 50; CRToExp[1/2] = 100; CRToExp[1] = 200; CRToExp[2] = 450; CRToExp[3] = 700; CRToExp[4] = 1100; CRToExp[5] = 1800; CRToExp[6] = 2300; CRToExp[7] = 2900; CRToExp[8] = 3900; CRToExp[9] = 5000; CRToExp[10] = 5900; CRToExp[11] = 7200; CRToExp[12] = 8400; CRToExp[13] = 10000; CRToExp[14] = 11500; CRToExp[15] = 13000; CRToExp[16] = 15000; CRToExp[17] = 18000; CRToExp[18] = 20000; CRToExp[19] = 22000; CRToExp[20] = 25000; CRToExp[21] = 33000; CRToExp[22] = 41000; CRToExp[23] = 50000; CRToExp[24] = 62000; CRToExp[25] = 75000; CRToExp[26] = 90000; CRToExp[27] = 105000; CRToExp[28] = 115000; CRToExp[29] = 135000; CRToExp[30] = 155000; }; BuildCRToExp(); class CountMultiplier { constructor(min, max, mult) { this.Min = min; this.Max = max; this.Mult = mult; } } const CountMultipliers = [ new CountMultiplier(0, 0, 0.5),// This is just for shifting due to party size. new CountMultiplier(1, 1, 1), new CountMultiplier(2, 2, 1.5), new CountMultiplier(3, 6, 2), new CountMultiplier(7, 10, 2.5), new CountMultiplier(11, 14, 3), new CountMultiplier(15, 999999999, 4), new CountMultiplier(999999999, 999999999, 4),// This is not in the DMG and is just for shifting due to party size ]; const GetCountMultiplier = (heroCount, monsterCount) => { let multIndex = -1; // Start at 1 because 0 is only shifted to due to party size. for (let i = 1; i < CountMultipliers.length; i++) { let mult = CountMultipliers[i]; if (mult.Min <= monsterCount && mult.Max >= monsterCount) { multIndex = i; break; } } if (heroCount < 3) { return CountMultipliers[multIndex+1].Mult; } else if (heroCount > 5) { return CountMultipliers[multIndex-1].Mult; } else { return CountMultipliers[multIndex].Mult; } }; const DailyExp = [ 300, 600, 1200, 1700, 3500, 4000, 5000, 6000, 7500, 9000, 10500, 11500, 13500, 15000, 18000, 20000, 25000, 27000, 30000, 40000 ]; class Player { constructor(name, level) { this.Name = name; this.Level = level; } } const GetPartyThresholds = () => { let easy = 0; let medium = 0; let hard = 0; let deadly = 0; state.DifficultyRating.Party.forEach((player) => { const level = player.Level; const expTier = XPThresholds[level-1]; easy += expTier[0]; medium += expTier[1]; hard += expTier[2]; deadly += expTier[3]; }); return [easy, medium, hard, deadly]; } const GetDailyExp = () => { let total = 0; state.DifficultyRating.Party.forEach((player) => { const level = player.Level; total += DailyExp[level-1]; }); return total; } const GetRollTemplate = () => { let rt = ''; if (state.DifficultyRating.Sheet === 'OGL') { rt = ['desc', 'desc']; } else if (state.DifficultyRating.Sheet === '5E-Shaped') { rt = ['5e-shaped', 'text']; } else if (state.DifficultyRating.Sheet === 'STARTUP') { log('Warning: Attempted to print from Difficulty Rating while still in STARTUP mode.'); return ['','']; } else { rt = ['default', `name=${drname} }}{{note`]; } return rt; } const PrintStatus = () => { const rt = GetRollTemplate(); let status = `/w gm &{template:${rt[0]}} {{${rt[1]}=