Last active
November 13, 2024 03:57
-
-
Save birdbrainiac/4a18d202ef43ab5fee938facecedb09e to your computer and use it in GitHub Desktop.
DC Heroes Dice Roller v1
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
/* 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