Skip to content

Instantly share code, notes, and snippets.

@birdbrainiac
Last active November 13, 2024 03:57
Show Gist options
  • Save birdbrainiac/4a18d202ef43ab5fee938facecedb09e to your computer and use it in GitHub Desktop.
Save birdbrainiac/4a18d202ef43ab5fee938facecedb09e to your computer and use it in GitHub Desktop.
DC Heroes Dice Roller v1
/* Structure
Input is in the form
!megs --av:sword:7 --ov:block:15
!megs --ev:strength:7 --ov:toughness:15 --shifts:3
!megs --av:7 --ov:15 --ev:7 --ov:15
!megs --title:full av --av:sword:7 --ov:block:15 --ev:strength:7 --ov:toughness:15 --attacker:wildstar --defender:batman
!megs --title:full av --av:sword:@{selected|sword} --ov:block:@{target|block} --attacker:@{target|character_name} --defender:@{selected|character_name --description:An av roll is made
--title: the topmost title
--attacker name of the attacker; if present forms part of the title
--defender name of the defender; if present forms part of the title
--av acting value, can be av:number, or av:name:number
--ov opposing value
--ev effect value
--rv resisting value
--omod shifts to the action table column
--rmod shifts to the effect table column
--description note added after title and befire roll
--note note added after roll effects
*/
// eslint-disable-next-line no-unused-vars
const dcheroes = (() => {
'use strict';
const editionFactor = 8;
const rollTarget = [ 3, 4, 5, 7, 9, 11, 13, 15, 18, 21, 24, 28, 32, 36, 40, 45, 50, 55, 60, 65, 70, 75, 80];
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
const rollTarget_0 = [6, 5, 4, 4, 3];
// eslint-disable-next-line no-unused-vars
const effectTable = [
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 0 1-2 3-4 5- 7- 9- 11 13 16 19 22 25 28 31 36 41 46 51 56
[ 'A', 1, 'N','N','N','N','N','N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 2, 1, 'N','N','N','N','N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 3, 2, 1, 'N','N','N','N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 5, 4, 3, 2, 'N','N','N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 8, 6, 4, 3, 2, 'N','N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 10, 9, 7, 6, 4, 3, 'N','N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 12, 11, 9, 8, 7, 5, 3, 'N','N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 14, 13, 11, 10, 9, 8, 6, 4, 'N','N','N','N','N','N','N','N','N','N' ],
[ 'A', 18, 17, 16, 14, 12, 10, 8, 6, 4, 'N','N','N','N','N','N','N','N','N' ],
[ 'A', 21, 20, 19, 17, 15, 13, 11, 9, 7, 5, 'N','N','N','N','N','N','N','N' ],
[ 'A', 24, 23, 22, 20, 18, 16, 14, 12, 10, 8, 6, 'N','N','N','N','N','N','N' ],
[ 'A', 27, 26, 25, 23, 21, 19, 17, 15, 13, 11, 9, 7, 'N','N','N','N','N','N' ],
[ 'A', 30, 29, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8, 'N','N','N','N','N' ],
[ 'A', 35, 34, 33, 31, 29, 27, 25, 23, 21, 19, 17, 14, 12, 9, 'N','N','N','N' ],
[ 'A', 40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 20, 18, 16, 13, 10, 'N','N','N' ],
[ 'A', 45, 43, 41, 40, 38, 36, 34, 31, 28, 26, 24, 22, 20, 17, 14, 11, 'N','N' ],
[ 'A', 50, 48, 46, 44, 42, 40, 38, 36, 34, 32, 30, 27, 24, 21, 18, 15, 12, 'N' ],
[ 'A', 55, 53, 51, 49, 47, 45, 43, 41, 39, 36, 33, 30, 27, 24, 21, 18, 15, 13 ]
];
const scale = [0, 2, 4, 6, 8, 10, 12, 15, 18, 21, 24, 27, 30, 36, 40, 45, 50, 55, 60];
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// get the row or column of a character's attribute.
const getLevel =(score) => (score > 60) ? Math.ceil((score-60)/editionFactor) +18 : scale.findIndex(lvl => score <= lvl);
const getRollLevel =(score) => (score > 80) ? Math.ceil((score-80)/editionFactor) +22 : rollTarget.findIndex(lvl => score < lvl);
// get roll ev - doesnt account for shifts off the edge of the table
const getDieString = (die1, die2) => `${(die1 === 10 ? 0 : die1)}${(die2 === 10 ? 0 : die2)}`;
const getRoll = () => {
let result = {total: 0, rolls: []};
let die1 = randomInteger(10);
let die2 = randomInteger(10);
result.rolls.push(getDieString(die1, die2));
result.total = die1 + die2;
if(die1 > 1) { // a double 1 always fails.
while(die1 === die2) {
die1 = randomInteger(10);
die2 = randomInteger(10);
result.rolls.push(getDieString(die1, die2));
result.total = result.total + die1 + die2;
}
}
return result;
};
const getSender = msg => {
const character = findObjs({ type: 'character', name: msg.who })[0],
player = getObj('player', msg.playerid);
if (character) return 'character|'+character.id;
if (player) return 'player|'+player.id;
return msg.who;
};
const getDieImage = roll => {
log('roll: ' + roll);
const die = 'JABCDEFGHI'; // 'jabcdefghi'
const die1 = parseInt(roll[0]) || 0;
const die2 = parseInt(roll[1]) || 0;
const letters = `${die[die1]}${die[die2]}`;
log(`die1: ${die1}; die2: ${die2}; letters: ${letters}`);
return `<span style="${die1===die2 ? (die1===1 ? 'color: red; ' : 'color: green; ') : '' }font-size:2.2em; font-family: 'dicefontd10';">${letters}</span>`;
};
const parseInput = (content) => {
const valid_values = ['av', 'ov', 'ev', 'rv', 'omod', 'rmod'];
const valid_descriptors = ['attacker', 'defender', 'note', 'description', 'title', 'shifts'];
const output = content.replace(/<br\/>/g, '') // delete added HTML line breaks
.replace(/\s+$/g, '') // delete trailing whitespace
.split(/\s+--/)
.slice(1)
.reduce((m, arg) => {
const prop = arg.split(':');
const keybase = prop[0].trim().toLowerCase();
if(prop.length === 1) {
m[keybase] = false; // if there is no :, just return the entry with value as false
} else if(valid_values.includes(keybase)) {
if(prop.length > 2) { // if 3 items, convert to name and value
m[`${keybase}_name`] = prop[1]; // eg: av: fist: 7 (probably should convert to a number)
m[`${keybase}_value`] = parseInt(prop[2]) || -1;
} else {
m[`${keybase}_value`] = parseInt(prop[1]) || -1;
}
} else if(valid_descriptors.includes(keybase)) {
m[keybase] = prop[1];
} else {
// not valid descriptor of value, add to error.
if (m.hasOwnProperty('invalid')) {
m.invalid.push(prop[1]);
} else {
m.invalid = [prop[1]];
}
}
return m;
}, {});
if(!output.hasOwnProperty('shifts')) output.shifts = 0;
if(!output.hasOwnProperty('omod_value')) output.omod_value = 0;
if(!output.hasOwnProperty('rmod_value')) output.rmod_value = 0;
return output;
};
const validateProps = (props) => {
/*
props might contain
title
attacker
defender
av_value
ov_value
ev_value
rv_value
omod_value
rmod_value
av_name
ov_name
ev_name
rv_name
omod_name
rmod_name
description
props MUST contain at least
av_value + ov_value
or
ev_value + rv_value
*/
};
// if roll is 11+ need to check if shifts occur
// shifts only count from 11
const getShifts =(target, roll) => {
const base_level = getRollLevel(roll);
const target_level = getRollLevel(target < 11 ? 9 : target) ;
return base_level > target_level ? base_level - target_level : 0;
};
const rollAction = (av, ov, mod = 0) => {
const output = {};
let av_rank = getLevel(av);
let ov_rank = getLevel(ov) + mod;
if(ov_rank < 0) {
av_rank - ov_rank;
ov_rank = 0;
}
if(ov === 0) {
output.target = rollTarget_0[Math.min(av_rank, 4)];
} else {
// difference = effective column to use; 5 = balanced, representing a needed roll of 11.
const difference = 5 + ov_rank - av_rank;
output.target = difference > 22 ? 80 + editionFactor * (difference -22) : rollTarget[Math.max(0, difference)];
}
output.roll = getRoll();
output.hit = (output.roll.total >= output.target) ? 1 : 0;
output.shifts = (output.hit && output.roll.total >10) ? getShifts(output.target, output.roll.total) : 0;
return output;
};
const rollEffect = (ev, rv, mod = 0, shifts = 0) => {
let RAP;
let ev_rank = getLevel(ev);
let rv_rank = getLevel(rv) + mod;
log(`ev rank: ${ev_rank}; rv_rank: ${rv_rank}; shifts: ${shifts}`);
if(ev_rank > 18 || rv_rank > 18) {
const difference = Math.max(ev_rank, rv_rank) - 18;
ev_rank -= difference;
rv_rank -= difference;
}
rv_rank -= shifts;
log(`ev rank: ${ev_rank}; rv_rank: ${rv_rank}`);
if(rv_rank <= 0) {
RAP = ev - rv_rank;
} else {
RAP = effectTable[ev_rank -1][rv_rank];
}
log(`row: ${effectTable[ev_rank -1].join('| ')}`);
return RAP;
};
const simplePrintout = (props, attack) => {
log(props);
log(attack);
const templateline = (key, value) => `{{${key}=${value}}}`;
const titleline = (entry) => `{{name=${entry}}}`;
// simplify the following code by checking if some quantities exist
const hit = attack.hasOwnProperty('hit') ? attack.hit : 0;
// 0 = no names, 1 = attacker only, 2 = attacker and defender
const characternames = props.hasOwnProperty('attacker') ? (props.hasOwnProperty('attacker') ? 2: 1) : 0;
const attacknames = props.hasOwnProperty('av_name') ? (props.hasOwnProperty('ev_name') ? 2: 1) : 0;
const defendnames = props.hasOwnProperty('ov_name') ? (props.hasOwnProperty('rv_name') ? 2: 1) : 0;
const attackvalues = (props.hasOwnProperty('ov_value') & props.hasOwnProperty('av_value')) ? 1: 0;
const defendvalues = (props.hasOwnProperty('ev_value') & props.hasOwnProperty('rv_value')) ? 1: 0;
const print = [];
print.push(`&{template:default}`);
let title = '';
if(props.hasOwnProperty('title')) title += props.title;
if(characternames > 0) {
if(props.hasOwnProperty('title')) title += ' (';
title += props.attacker;
if(characternames >1) title += ` vs ${props.defender}`;
if(props.hasOwnProperty('title')) title += ')';
}
if(!title) title = 'Action Roll';
print.push(titleline(title));
if(props.hasOwnProperty('description')) print.push(templateline('description', props.description));
let attackline = '';
if(attackvalues > 0) attackline = `${attacknames > 0 ? props.av_name: 'AV'}: ${props.av_value}`;
if(hit && attackvalues > 0) attackline += `; ${attacknames > 1 ? props.ev_name: 'EV'}: ${props.ev_value}`;
print.push(templateline('Attack', attackline));
let defendline = '';
if(defendvalues > 0) defendline = `${defendnames > 0 ? props.ov_name: 'OV'}: ${props.ov_value}`;
if(hit && defendvalues > 0) defendline += `; ${defendnames > 1 ? props.rv_name: 'EV'}: ${props.rv_value}`;
print.push(templateline('Defence', defendline));
// add line for manoevers
if(attackvalues > 0) {
if(props.hasOwnProperty('omod_value') && props.omod_value !== 0) {
print.push(templateline('O-Mod', `${props.hasOwnProperty('omod_name') ? `${props.omod_name}: ` : ''}${props.omod_value >= 0 ? '+': ''}${props.omod_value}`));
}
print.push(templateline('Target', attack.target));
let roll = `${attack.roll.rolls.map(r => getDieImage(r)).join(' ')}`;
print.push(templateline('Roll', roll));
print.push(templateline('Total', attack.roll.total));
print.push(templateline('Result', hit ? `Hit ${attack.shifts ? ` with ${attack.shifts} Shifts` : ''}` : 'Miss'));
}
if(hit && defendvalues > 0 && attack.hasOwnProperty('raps')) {
if(props.hasOwnProperty('rmod_value') && props.rmod_value !== 0) {
print.push(templateline('R-Mod', `${props.hasOwnProperty('rmod_name') ? `${props.rmod_name}: ` : ''}${props.rmod_value >= 0 ? '+': ''}${props.rmod_value}`));
}
print.push(templateline('RAPs', attack.raps));
}
if(props.hasOwnProperty('note')) print.push(templateline('Note', props.note));
return print.join(' ');
};
const handleInput = (msg) => {
if (msg.type === 'api' && msg.content.startsWith('!megs') )
{
const sender = getSender(msg);
const props = parseInput(msg.content);
log(props);
let attack = {};
if(props.hasOwnProperty('av_value') && props.hasOwnProperty('ov_value')) {
attack = rollAction(props.av_value, props.ov_value, props.omod_value);
}
// temporary printout for testing
if(props.hasOwnProperty('ev_value') && props.hasOwnProperty('rv_value')) {
attack.raps = rollEffect(props.ev_value, props.rv_value, props.rmod_value, attack.shifts);
}
const simpleprint = simplePrintout(props, attack);
sendChat(sender,simpleprint);
}
};
const registerEventHandlers = () => {
on('chat:message', handleInput);
};
on('ready', () => {
'use strict';
registerEventHandlers();
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment