mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-09-05 06:32:31 +00:00
344 lines
13 KiB
JavaScript
344 lines
13 KiB
JavaScript
/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
|
||
/*jshint -W014,-W084,-W030,-W033*/
|
||
//Roll Listeners
|
||
const rollBasicMove = async function({trigger,attributes,sections}){
|
||
const [,,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
|
||
let statRef = getMoveResults(field.replace(/-/g,' '),rollObj,attributes);
|
||
rollObj.roll = assembleRoll(dice,statRef,activeEffects,attributes.is_weird);
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollBasicMove});
|
||
|
||
const getMoveResults = function(move,rollObj,attributes){
|
||
const possibleResults = basicMoves[move];
|
||
['critical','success','mixed','failure','fumble'].forEach((result)=>{
|
||
if(result !== 'stat'){
|
||
rollObj[`${result}_result`] = possibleResults[result] ?
|
||
`^{${possibleResults[result]}}` :
|
||
'';
|
||
}
|
||
});
|
||
return attributes[`${move.replace(/\s+/g,'_')}_stat`];
|
||
};
|
||
|
||
const rollStat = async function({trigger,attributes,sections}){
|
||
const [,,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
|
||
rollObj.roll = assembleRoll(dice,`@{${field}}`,activeEffects,attributes.is_weird);
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollStat});
|
||
|
||
const rollSkill = async function({trigger,attributes,sections}){
|
||
const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections,isSkill:true});
|
||
let statRef;
|
||
rollObj.header = attributes[`${section}_${rowID}_${field === 'inability' ? field : 'name'}`];
|
||
if(attributes.skill_basic_move){
|
||
const query = k.capitalize(getTranslationByKey('move query'));
|
||
const options = Object.keys(basicMoves)
|
||
.map((key) => {
|
||
return `${k.capitalize(getTranslationByKey(key))},${key}`
|
||
})
|
||
.join('|');
|
||
const move = await k.extractQueryResult(`${query}|${options}`);
|
||
statRef = getMoveResults(move,rollObj,attributes);
|
||
}else{
|
||
await k.pseudoQuery('');
|
||
statRef = section === 'repeating_skill' ?
|
||
(/inability/.test(field) ?
|
||
'inability_stat' :
|
||
'skill_stat'
|
||
) :
|
||
'stat';
|
||
statRef = attributes[`${section}_${rowID}_${statRef}`];
|
||
}
|
||
k.debug({section,rowID,field});
|
||
|
||
rollObj.roll = assembleRoll(dice,statRef,activeEffects,attributes.is_weird);
|
||
k.debug({skillObj:rollObj});
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollSkill});
|
||
|
||
const rollHelpFriend = async function({trigger,attributes,sections}){
|
||
const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
|
||
const relationScore = attributes[`${section}_${rowID}_score`];
|
||
rollObj.header = getTranslationByKey('help a friend roll header').replace('{{0}}',attributes[`${section}_${rowID}_name`]);
|
||
k.debug({activeEffects});
|
||
rollObj.roll = assembleRoll(dice,relationScore,activeEffects,attributes.is_weird);
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollHelpFriend});
|
||
|
||
const rollCharacterMove = async function({trigger,attributes,sections}){
|
||
const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
|
||
rollObj.roll = assembleRoll(dice,'?{query}',activeEffects,attributes.is_weird);
|
||
const fieldSwitch = {
|
||
critical_result:attributes[`${section}_${rowID}_critical_success`] || attributes[`${section}_${rowID}_full_success`] || basicMoves['another move'].critical,
|
||
success_result:attributes[`${section}_${rowID}_full_success`] || basicMoves['another move'].success,
|
||
mixed_result:attributes[`${section}_${rowID}_mixed_success`] || basicMoves['another move'].mixed,
|
||
failure_result:attributes[`${section}_${rowID}_failure`] || basicMoves['another move'].failure,
|
||
fumble_result:attributes[`${section}_${rowID}_critical_failure`] || attributes[`${section}_${rowID}_failure`] || basicMoves['another move'].fumble
|
||
};
|
||
rollObj.header = attributes[`${section}_${rowID}_name`];
|
||
Object.entries(fieldSwitch).forEach(([fieldName,content])=>{
|
||
rollObj[fieldName] = content;
|
||
});
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollCharacterMove});
|
||
|
||
const rollInitiative = async function({trigger,attributes,sections}){
|
||
const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
|
||
rollObj.roll = assembleRoll(dice,attributes.fierce,activeEffects,attributes.is_weird)
|
||
.replace(/\]\]/,'&{tracker}]]');
|
||
executeRoll({rollObj});
|
||
};
|
||
k.registerFuncs({rollInitiative});
|
||
|
||
//Parse Functions
|
||
const baseRollInfo = async function({trigger,attributes,sections,isSkill}){
|
||
const [section,rowID,button] = k.parseTriggerName(trigger.name);
|
||
const field = button.replace(/-?action/,'');
|
||
const baseEffects = {advantage:0,bonus:0,text:[]};
|
||
if(isSkill){
|
||
baseEffects.advantage = field === 'inability' ?
|
||
-1 - attributes[`${section}_${rowID}_${field}_super`] :
|
||
1 + attributes[`${section}_${rowID}_super`];
|
||
}
|
||
const activeEffects = await countEffects(attributes,sections,baseEffects);
|
||
const rollObj = {
|
||
header:`^{${field.replace(/-/g,' ')}}`,
|
||
result:`[[0[computed value]]]`
|
||
};
|
||
if(baseEffects.text?.length){
|
||
rollObj.description = baseEffects.text.map((text)=>`^{${text}}`).join('\n');
|
||
}
|
||
Object.entries(basicMoves['another move']).forEach(([result,effect])=>{
|
||
if(result !== 'stat'){
|
||
rollObj[`${result}_result`] = `^{${effect}}`;
|
||
}
|
||
});
|
||
const dice = await parseAdvantage(attributes.advantage,activeEffects,attributes,rollObj);
|
||
return [section,rowID,field,activeEffects,rollObj,dice];
|
||
};
|
||
|
||
/*
|
||
The Compatibility Scores and their effects are as follows:
|
||
-1 (Frenemies)
|
||
The other PC rolls with Disadvantage. Each of you gains 1 AP.
|
||
|
||
+0 (Acquaintances)
|
||
The other PC gets +1 on their roll.
|
||
|
||
+1 (Friends)
|
||
CHOOSE 1
|
||
The other PC gets +1 on their roll.
|
||
The consequences of the other PC’s actions are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
|
||
|
||
+2 (Good Friends)
|
||
CHOOSE 1
|
||
The other PC gets +2 on their roll.
|
||
The other PC rolls with Advantage.
|
||
The consequences of the other PC’s actions are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
|
||
|
||
+3 (Family)
|
||
CHOOSE UP TO 2
|
||
The other PC gets +2 on their roll.
|
||
The other PC rolls with Advantage.
|
||
The consequences of the other PC’s move are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
|
||
*/
|
||
const compatibilityQuery = async function(baseEffects,attributes,sections){
|
||
const compatibleEffects = {
|
||
'-1':{optNum:1,options:['disadvantage']},
|
||
'0':{optNum:1,options:['+1']},
|
||
'1':{optNum:1,options:['+1','bigger consequences']},
|
||
'2':{optNum:1,options:['+1','advantage','bigger consequences']},
|
||
'3':{optNum:2,options:['+2','advantage','bigger consequences']}
|
||
}
|
||
const activeID = sections.repeating_compatibility.reduce((memo,id)=>{
|
||
return attributes[`repeating_compatibility_${id}_activate`] &&
|
||
(!memo || attributes[`repeating_compatibility_${id}_score`] < attributes[`repeating_compatibility_${id}_score`]) ?
|
||
id :
|
||
memo;
|
||
},'');
|
||
const ordinals = {
|
||
'1':'1st',
|
||
'2':'2nd'
|
||
}
|
||
if(activeID){
|
||
const activeEffect = compatibleEffects[attributes[`repeating_compatibility_${activeID}_score`]];
|
||
k.debug({activeID,[`compatibleEffects[${attributes[`repeating_compatibility_${activeID}_score`]}]`]:activeEffect})
|
||
const selectedEffects = [];
|
||
if(activeEffect.options.length > 1){
|
||
const effectWorker = async (iteration = 1)=>{
|
||
const availableOptions = activeEffect.options.filter((opt)=>selectedEffects.indexOf(opt) === -1);
|
||
const promptKey = activeEffect.optNum === 1 ?
|
||
'Which compatibility effect' :
|
||
`${ordinals[iteration]} compatible effect`;
|
||
const selectedOption = await k.extractQueryResult(`${getTranslationByKey(promptKey)}|${availableOptions.map((opt)=>`${getTranslationByKey(`compatibility prompt ${opt}`)},${opt}`).join('|')}`);
|
||
k.debug({selectedOption});
|
||
selectedOption.replace(/\+?(.+)/,(match,effect)=>{
|
||
if(+effect){
|
||
baseEffects.bonus += +effect;
|
||
}else if(/dis/.test(effect)){
|
||
baseEffects.advantage--;
|
||
}else if(/advantage/.test(effect)){
|
||
baseEffects.advantage++;
|
||
}else{
|
||
baseEffects.text.push(effect);
|
||
}
|
||
});
|
||
selectedEffects.push(selectedOption);
|
||
iteration++;
|
||
if(iteration > activeEffect.optNum){
|
||
return true;
|
||
}else{
|
||
return effectWorker(iteration);
|
||
}
|
||
}
|
||
await effectWorker();
|
||
}else{
|
||
selectedEffects.push(activeEffect.options[0]);
|
||
await k.pseudoQuery();
|
||
}
|
||
}else{
|
||
if(attributes.interference){
|
||
baseEffects.advantage--;
|
||
baseEffects.text.push('interference roll message');
|
||
}
|
||
await k.pseudoQuery();
|
||
}
|
||
};
|
||
|
||
const countEffects = async function(attributes,sections,effectObj = {advantage:0,bonus:0}){
|
||
let idArray;
|
||
const baseEffects = {...effectObj};
|
||
if(attributes.use_roll_modifiers){
|
||
idArray = sections.repeating_modifier;
|
||
}else{
|
||
idArray = [];
|
||
baseEffects.bonus = attributes.generic_bonus;
|
||
}
|
||
await compatibilityQuery(baseEffects,attributes,sections);
|
||
k.debug({baseEffects});
|
||
return idArray.reduce((memo,id)=>{
|
||
k.debug({[`attributes[repeating_modifier_${id}_activate]`]:attributes[`repeating_modifier_${id}_activate`]});
|
||
if(attributes[`repeating_modifier_${id}_activate`]){
|
||
const effect = attributes[`repeating_modifier_${id}_effect`];
|
||
k.debug({effect});
|
||
const advs = effect.match(/(?:^|\s)(?:super )?advantage/ig) || [];
|
||
const disadvs = effect.match(/(?:super )?disadvantage/ig) || [];
|
||
const bonuses = effect.match(/(?:[+\-]\s*)?\d+/g) || [];
|
||
k.debug({advs,disadvs,bonuses});
|
||
memo.advantage = advs.reduce((total,adv)=>total += /super/.test(adv) ? 2 : 1,memo.advantage);
|
||
memo.advantage = disadvs.reduce((total,adv)=>total -= /super/.test(adv) ? 2 : 1,memo.advantage);
|
||
memo.bonus = bonuses.reduce((total,bonus)=>{
|
||
const val = +bonus.replace(/\+/,'').trim();
|
||
return total += val;
|
||
},memo.bonus);
|
||
}
|
||
return memo;
|
||
},{...baseEffects});
|
||
};
|
||
|
||
const parseAdvantage = async function(advantageState,activeEffects,attributes,rollObj){
|
||
if(attributes.use_physical_dice){
|
||
const result = +(await k.extractQueryResult(getTranslationByKey('physical roll query')));
|
||
if(result === 12){
|
||
rollObj.result = '[[1]]';
|
||
}else if(result === 2){
|
||
rollObj.result = '[[-1]]';
|
||
}else{
|
||
rollObj.result = '[[0]]';
|
||
}
|
||
return result;
|
||
}else{
|
||
await k.pseudoQuery('');
|
||
if(advantageState.startsWith('?{')){
|
||
//If the advantage state is a query, translate the query.
|
||
const options = Object.entries(advantageTypes).map(([aType,aValue])=>{
|
||
return `${k.capitalize(getTranslationByKey(`${aType} option`))},${aValue}`;
|
||
}).join('|');
|
||
return `?{${k.capitalize(getTranslationByKey('roll'))}|${options}}`;
|
||
}else{
|
||
//otherwise, just use the advantage as is.
|
||
return activeEffects.advantage ?
|
||
(() => {
|
||
const diceNum = 2 + Math.abs(activeEffects.advantage);
|
||
const keep = activeEffects.advantage > 0 ?
|
||
'kh' :
|
||
'kl';
|
||
return `${diceNum}d6${keep}2`;
|
||
})() :
|
||
advantageState;
|
||
}
|
||
}
|
||
};
|
||
|
||
const assembleRoll = function(dice,chosenStat,activeEffects,isWeird){
|
||
let stat;
|
||
k.debug({chosenStat,activeEffects});
|
||
if(`${chosenStat}`.startsWith('?{')){
|
||
//If the advantage state is a query, translate the query.
|
||
const options = attributeNames.map((attr)=>{
|
||
if(attr !== 'weird' || isWeird){
|
||
return `${k.capitalize(getTranslationByKey(attr))},@{${attr}}`;
|
||
}else{
|
||
return null;
|
||
}
|
||
})
|
||
.filter((a)=>a)
|
||
.join('|');
|
||
stat = `+ ?{${k.capitalize(getTranslationByKey('which stat'))}|${options}}`;
|
||
}else{
|
||
//otherwise, just use the advantage as is.
|
||
stat = `+ ${chosenStat}`;
|
||
}
|
||
const bonus = stringifyBonus(activeEffects.bonus);
|
||
return `[[${dice}${stat}${bonus}]]`;
|
||
};
|
||
|
||
const stringifyBonus = function(bonus){
|
||
k.debug({bonus});
|
||
return bonus < 0 ?
|
||
bonus :
|
||
`+ ${bonus}`.replace(/\+ 0/,'');
|
||
};
|
||
|
||
const executeRoll = async function({rollObj}){
|
||
k.debug({toExecute:rollObj});
|
||
let message = `@{template_start} ${Object.entries(rollObj).map(([field,content])=>`{{${field}=${content}}}`).join('')}`;
|
||
k.debug({message});
|
||
const rolled = await startRoll(message);
|
||
const computeObj = {};
|
||
if(rolled.results.roll && rolled.results.result && /computed value/.test(rollObj.result)){
|
||
const expression = rolled.results.roll.expression;
|
||
let rollState;
|
||
let rollQuant = 2;
|
||
expression.replace(/d6k([hl])(\d+)/,(match,target,num)=>{
|
||
rollState = target;
|
||
rollQuant = +num || 2;
|
||
});
|
||
const diceRoll = rolled.results.roll.rolls[0];
|
||
const rollTotal = diceRoll.results.sort((a,b)=>{
|
||
if(rollState === 'l'){
|
||
return a - b;
|
||
}else if(rollState === 'h'){
|
||
return b - a;
|
||
}else{
|
||
return 0;
|
||
}
|
||
})
|
||
.slice(0,rollQuant);
|
||
k.debug({rollTotal});
|
||
const uniqueRolls = [...new Set(rollTotal)];
|
||
if(uniqueRolls.length === 1){
|
||
if(uniqueRolls[0] === 6){
|
||
computeObj.result = 1;
|
||
}else if(uniqueRolls[0] === 1){
|
||
computeObj.result = -1;
|
||
}
|
||
}
|
||
}
|
||
finishRoll(rolled.rollId,computeObj);
|
||
}; |